链接器如何跨翻译单元处理相同的模板实例化? [英] How does the linker handle identical template instantiations across translation units?

查看:21
本文介绍了链接器如何跨翻译单元处理相同的模板实例化?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

假设我有两个翻译单元:

Suppose I have two translation-units:

foo.cpp

void foo() {
  auto v = std::vector<int>();
}

bar.cpp

void bar() {
  auto v = std::vector<int>();
}

当我编译这些翻译单元时,每个都会实例化 std::vector<int>.

When I compile these translation-units, each will instantiate std::vector<int>.

我的问题是:这在链接阶段是如何工作的?

My question is: how does this work at the linking stage?

  • 两个实例化的名称是否不同?
  • 链接器是否将它们作为重复项删除?

推荐答案

C++ 要求 内联函数定义存在于引用该函数的翻译单元中.模板成员函数是隐式内联的,但默认情况下也使用外部实例化连锁.因此,当定义重复时,链接器将可见同一个模板用不同的模板参数实例化翻译单位.链接器如何处理这种重复是你的问题.

C++ requires that an inline function definition be present in a translation unit that references the function. Template member functions are implicitly inline, but also by default are instantiated with external linkage. Hence the duplication of definitions that will be visible to the linker when the same template is instantiated with the same template arguments in different translation units. How the linker copes with this duplication is your question.

您的 C++ 编译器受制于 C++ 标准,但您的链接器不受制于任何关于如何链接 C++ 的编纂标准:它本身就是一条法律,植根于计算历史,对对象的源语言漠不关心代码它链接.您的编译器必须使用目标链接器可以并且将这样做,以便您可以成功链接您的程序并看到它们你所期望的.因此,我将向您展示 GCC C++ 编译器如何与GNU 链接器来处理不同翻译单元中的相同模板实例化.

Your C++ compiler is subject to the C++ Standard, but your linker is not subject to any codified standard as to how it shall link C++: it is a law unto itself, rooted in computing history and indifferent to the source language of the object code it links. Your compiler has to work with what a target linker can and will do so that you can successfully link your programs and see them do what you expect. So I'll show you how the GCC C++ compiler interworks with the GNU linker to handle identical template instantiations in different translation units.

这个演示利用了一个事实,虽然 C++ 标准要求 -通过 一个定义规则- 同一模板的不同翻译单元中的实例化相同的模板参数应具有相同的定义,编译器 -当然 - 不能对不同之间的关系强制执行任何要求翻译单位.它必须信任我们.

This demonstration exploits the fact that while the C++ Standard requires - by the One Definition Rule - that the instantiations in different translation units of the same template with the same template arguments shall have the same definition, the compiler - of course - cannot enforce any requirement like that on relationships between different translation units. It has to trust us.

所以我们会用不同的参数实例化同一个模板翻译单元,但我们会通过注入宏观控制​​的差异来作弊随后将显示的不同翻译单元中的实现我们链接器选择了哪个定义.

So we'll instantiate the same template with the same parameters in different translation units, but we'll cheat by injecting a macro-controlled difference into the implementations in different translation units that will subsequently show us which definition the linker picks.

如果您怀疑此作弊使演示无效,请记住:编译器无法知道 ODR 是否曾经在不同的翻译单元中得到尊重,所以它不能在那个帐户上表现不同,而且没有这样的事情作为欺骗"链接器.无论如何,演示会证明它是有效的.

If you suspect this cheat invalidates the demonstration, remember: the compiler cannot know whether the ODR is ever honoured across different translation units, so it cannot behave differently on that account, and there's no such thing as "cheating" the linker. Anyhow, the demo will demonstrate that it is valid.

首先我们有我们的作弊模板头:

First we have our cheat template header:

thing.hpp

#ifndef THING_HPP
#define THING_HPP
#ifndef ID
#error ID undefined
#endif

template<typename T>
struct thing
{
    T id() const {
        return T{ID};
    }
};

#endif

ID的值就是我们可以注入的tracer值.

The value of the macro ID is the tracer value we can inject.

下一个源文件:

foo.cpp

#define ID 0xf00
#include "thing.hpp"

unsigned foo()
{
    thing<unsigned> t;
    return t.id();
}

它定义了函数foo,其中thing是实例化定义t,并返回t.id().通过成为一个函数实例化 thing<unsigned> 的外部链接,foo 服务于目的的:-

It defines function foo, in which thing<unsigned> is instantiated to define t, and t.id() is returned. By being a function with external linkage that instantiates thing<unsigned>, foo serves the purposes of:-

  • 强制编译器进行实例化
  • 在链接中公开实例化,以便我们可以探查什么链接器使用它.

另一个源文件:

boo.cpp

#define ID 0xb00
#include "thing.hpp"

unsigned boo()
{
    thing<unsigned> t;
    return t.id();
}

这就像 foo.cpp 除了它定义 boo 代替 foo 和设置 ID = 0xb00.

which is just like foo.cpp except that it defines boo in place of foo and sets ID = 0xb00.

最后是程序源:

ma​​in.cpp

#include <iostream>

extern unsigned foo();
extern unsigned boo();

int main()
{
    std::cout << std::hex 
    << '
' << foo()
    << '
' << boo()
    << std::endl;
    return 0;
}

这个程序将以十六进制打印 foo() 的返回值——我们的作弊应该做的= f00 - 然后是 boo() 的返回值 - 我们的作弊应该做的 = b00.

This program will print, as hex, the return value of foo() - which our cheat should make = f00 - then the return value of boo() - which our cheat should make = b00.

现在我们将编译 foo.cpp,我们将使用 -save-temps 来编译,因为我们想要看看组装:

Now we'll compile foo.cpp, and we'll do it with -save-temps because we want a look at the assembly:

g++ -c -save-temps foo.cpp

这会在 foo.s 中写入程序集,感兴趣的部分是thing::id() const 的定义(mangled = _ZNK5thingIjE2idEv):

This writes the assembly in foo.s and the portion of interest there is the definition of thing<unsigned int>::id() const (mangled = _ZNK5thingIjE2idEv):

    .section    .text._ZNK5thingIjE2idEv,"axG",@progbits,_ZNK5thingIjE2idEv,comdat
    .align 2
    .weak   _ZNK5thingIjE2idEv
    .type   _ZNK5thingIjE2idEv, @function
_ZNK5thingIjE2idEv:
.LFB2:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movq    %rdi, -8(%rbp)
    movl    $3840, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc

顶部的三个指令很重要:

Three of the directives at the top are significant:

.section    .text._ZNK5thingIjE2idEv,"axG",@progbits,_ZNK5thingIjE2idEv,comdat

这个将函数定义放在它自己的链接部分中,称为.text._ZNK5thingIjE2idEv 将被输出,如果需要,合并到.text (即代码)链接目标文件的程序部分.一个像这样的链接部分,即 .text. 称为 function-section.这是一个代码段,只包含函数<function_name>的定义.

This one puts the function definition in a linkage section of its own called .text._ZNK5thingIjE2idEv that will be output, if it's needed, merged into the .text (i.e. code) section of program in which the object file is linked. A linkage section like that, i.e. .text.<function_name> is called a function-section. It's a code section that contains only the definition of function <function_name>.

指令:

.weak   _ZNK5thingIjE2idEv

至关重要.它将 thing<unsigned int>::id() const 归类为 弱a> 符号.GNU 链接器识别 strong 符号和 weak 符号.对于一个强符号,链接器将只接受链接中的一个定义.如果有更多,它将给出一个倍数-定义错误.但是对于一个弱符号,它可以容忍任意数量的定义,并选择一个.如果一个弱定义的符号在链接中也有(只有一个)强定义,那么将选择强定义.如果一个符号有多个弱定义而没有强定义,然后链接器可以任意选择任何一个弱定义.

is crucial. It classifies thing<unsigned int>::id() const as a weak symbol. The GNU linker recognises strong symbols and weak symbols. For a strong symbol, the linker will accept only one definition in the linkage. If there are more, it will give a multiple -definition error. But for a weak symbol, it will tolerate any number of definitions, and pick one. If a weakly defined symbol also has (just one) strong definition in the linkage then the strong definition will be picked. If a symbol has multiple weak definitions and no strong definition, then the linker can pick any one of the weak definitions, arbitrarily.

指令:

.type   _ZNK5thingIjE2idEv, @function

thing::id() 归类为引用一个 函数 - 而不是数据.

classifies thing<unsigned int>::id() as referring to a function - not data.

然后在定义体中,在地址处组装代码由弱全局符号_ZNK5thingIjE2idEv标记,本地相同标记为 .LFB2.代码返回 3840 (= 0xf00).

Then in the body of the definition, the code is assembled at the address labelled by the weak global symbol _ZNK5thingIjE2idEv, the same one locally labelled .LFB2. The code returns 3840 ( = 0xf00).

接下来我们用同样的方式编译boo.cpp:

Next we'll compile boo.cpp the same way:

g++ -c -save-temps boo.cpp

再看看 thing::id()boo.s

    .section    .text._ZNK5thingIjE2idEv,"axG",@progbits,_ZNK5thingIjE2idEv,comdat
    .align 2
    .weak   _ZNK5thingIjE2idEv
    .type   _ZNK5thingIjE2idEv, @function
_ZNK5thingIjE2idEv:
.LFB2:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movq    %rdi, -8(%rbp)
    movl    $2816, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc

除了我们的作弊之外,它是相同的:这个定义返回 2816 (= 0xb00).

It's identical, except for our cheat: this definition returns 2816 ( = 0xb00).

当我们在这里时,让我们注意一些可能不言而喻的事情:一旦我们进入汇编(或目标代码),类就消失了.这里,我们的目标是:-

While we're here, let's note something that might or might not go without saying: Once we're in assembly (or object code), classes have evaporated. Here, we're down to: -

  • 数据
  • 代码
  • 符号,可以标注数据或标注代码.

所以这里没有什么特别代表实例化 thing<T> forT = 无符号.在这种情况下,thing<unsigned> 剩下的就是_ZNK5thingIjE2idEv 的定义也就是 thing::id() const.

So nothing here specifically represents the instantiation of thing<T> for T = unsigned. All that's left of thing<unsigned> in this instance is the definition of _ZNK5thingIjE2idEv a.k.a thing<unsigned int>::id() const.

所以现在我们知道编译器在实例化thing<unsigned>方面做了什么在给定的翻译单元中.如果它必须实例化一个 thing<unsigned>成员函数,然后它组装实例化成员的定义函数在一个弱全局符号上,它标识了成员函数,并且它将此定义放入其自己的功能部分.

So now we know what the compiler does about instantiating thing<unsigned> in a given translation unit. If it is obliged to instantiate a thing<unsigned> member function, then it assembles the definition of the instantiated member function at a weakly global symbol that identifies the member function, and it puts this definition into its own function-section.

现在让我们看看链接器做了什么.

Now let's see what the linker does.

首先我们将编译主源文件.

First we'll compile the main source file.

g++ -c main.cpp

然后链接所有对象文件,请求对 _ZNK5thingIjE2idEv 的诊断跟踪,和一个链接映射文件:

Then link all the object files, requesting a diagnostic trace on _ZNK5thingIjE2idEv, and a linkage map file:

g++ -o prog main.o foo.o boo.o -Wl,--trace-symbol='_ZNK5thingIjE2idEv',-M=prog.map
foo.o: definition of _ZNK5thingIjE2idEv
boo.o: reference to _ZNK5thingIjE2idEv

所以链接器告诉我们程序从foo.o 并在 boo.o调用它.

So the linker tells us that the program gets the definition of _ZNK5thingIjE2idEv from foo.o and calls it in boo.o.

运行程序表明它说的是真话:

Running the program shows it's telling the truth:

./prog

f00
f00

foo()boo() 都返回 thing().id() 的值 foo.cpp 中实例化.

Both foo() and boo() are returning the value of thing<unsigned>().id() as instantiated in foo.cpp.

thing::id() const 的other 定义变成了什么boo.o 中?地图文件向我们展示了:

What has become of the other definition of thing<unsigned int>::id() const in boo.o? The map file shows us:

prog.map

...
Discarded input sections
 ...
 ...
 .text._ZNK5thingIjE2idEv
                0x0000000000000000        0xf boo.o
 ...
 ...

链接器丢弃了 boo.o 中的函数部分包含另一个定义.

The linker chucked away the function-section in boo.o that contained the other definition.

现在让我们再次链接 prog,但这次是在 foo.oboo.o 中逆序:

Let's now link prog again, but this time with foo.o and boo.o in the reverse order:

$ g++ -o prog main.o boo.o foo.o -Wl,--trace-symbol='_ZNK5thingIjE2idEv',-M=prog.map
boo.o: definition of _ZNK5thingIjE2idEv
foo.o: reference to _ZNK5thingIjE2idEv

这一次,程序从boo.o中得到_ZNK5thingIjE2idEv的定义,并在 foo.o 中调用它.该程序确认:

This time, the program gets the definition of _ZNK5thingIjE2idEv from boo.o and calls it in foo.o. The program confirms that:

$ ./prog

b00
b00

地图文件显示:

...
Discarded input sections
 ...
 ...
 .text._ZNK5thingIjE2idEv
                0x0000000000000000        0xf foo.o
 ...
 ...

链接器丢弃了函数部分 .text._ZNK5thingIjE2idEv来自 foo.o.

that the linker chucked away the function-section .text._ZNK5thingIjE2idEv from foo.o.

这幅画就完成了.

编译器在每个翻译单元中发出一个弱定义每个实例化的模板成员都在其自己的函数部分中.链接器然后只选择它遇到的那些弱定义中的 first在链接序列中需要解析弱引用时象征.因为每个弱符号都针对一个定义,所以任何其中一个 - 特别是第一个 - 可用于解析所有引用链接中的符号,其余的弱定义是消耗品.多余的弱定义必须被忽略,因为链接器只能链接给定符号的一个定义.还有剩余链接器可以丢弃弱定义,没有抵押对程序造成损害,因为编译器将每一个都单独放在一个链接部分中.

The compiler emits, in each translation unit, a weak definition of each instantiated template member in its own function section. The linker then just picks the first of those weak definitions that it encounters in the linkage sequence when it needs to resolve a reference to the weak symbol. Because each of the weak symbols addresses a definition, any one one of them - in particular, the first one - can be used to resolve all references to the symbol in the linkage, and the rest of the weak definitions are expendable. The surplus weak definitions must be ignored, because the linker can only link one definition of a given symbol. And the surplus weak definitions can be discarded by the linker, with no collateral damage to the program, because the compiler placed each one in a linkage section all by itself.

通过选择它看到的 first 弱定义,链接器实际上是随机选择,因为目标文件的链接顺序是任意的.但这很好,只要我们遵守跨多个翻译单元的 ODR,因为我们这样做了,所以所有的弱定义确实是相同的.#include 的通常做法是从头文件的任何地方对类模板进行处理(而不是在我们这样做时宏注入任何本地编辑)是遵守规则的一种相当稳健的方式.

By picking the first weak definition it sees, the linker is effectively picking at random, because the order in which object files are linked is arbitrary. But this is fine, as long as we obey the ODR accross multiple translation units, because it we do, then all of the weak definitions are indeed identical. The usual practice of #include-ing a class template everywhere from a header file (and not macro-injecting any local edits when we do so) is a fairly robust way of obeying the rule.

这篇关于链接器如何跨翻译单元处理相同的模板实例化?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆