简体   繁体   English

链接器如何处理跨翻译单元的相同模板实例化?

[英]How does the linker handle identical template instantiations across translation units?

Suppose I have two translation-units:假设我有两个翻译单元:

foo.cpp文件

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

bar.cpp bar.cpp

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

When I compile these translation-units, each will instantiate std::vector<int> .当我编译这些翻译单元时,每个单元都会实例化std::vector<int>

My question is: how does this work at the linking stage?我的问题是:这在链接阶段如何工作?

  • Do both instantiations have different mangled names?两个实例是否具有不同的重整名称?
  • Does the linker remove them as duplicates?链接器是否将它们作为重复项删除?

C++ requires that an inline function definition be present in a translation unit that references the function. C++要求在引用函数的翻译单元中存在内联函数定义 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.链接器如何处理这种重复是您的问题。

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.你的 C++ 编译器受 C++ 标准的约束,但你的链接器不受任何关于它如何链接 C++ 的编纂标准的约束:它本身就是一条法则,植根于计算历史,对它的目标代码的源语言漠不关心链接。 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.因此,我将向您展示 GCC C++ 编译器如何与 GNU 链接器交互以处理不同翻译单元中相同的模板实例。

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.该演示利用了这样一个事实:虽然 C++ 标准要求- 根据一个定义规则- 具有相同模板参数的同一模板的不同翻译单元中的实例化应具有相同的定义,但编译器 - 当然 - 不能强制执行任何要求就像不同翻译单元之间的关系一样。 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.因此,我们将在不同的翻译单元中使用相同的参数实例化相同的模板,但我们将通过将宏控制的差异注入不同翻译单元中的实现来作弊,随后将向我们显示链接器选择的定义。

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.如果您怀疑此作弊使演示无效,请记住:编译器无法知道 ODR 是否在不同的翻译单元中得到遵守,因此在该帐户上它的行为不会有所不同,并且不存在“欺骗”链接器这样的事情。 Anyhow, the demo will demonstrate that it is valid.无论如何,演示将证明它是有效的。

First we have our cheat template header:首先,我们有我们的作弊模板标题:

thing.hpp东西.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

The value of the macro ID is the tracer value we can inject.ID的值是我们可以注入的跟踪器值。

Next a source file:接下来是源文件:

foo.cpp文件

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

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

It defines function foo , in which thing<unsigned> is instantiated to define t , and t.id() is returned.它定义了函数foo ,其中thing<unsigned>被实例化以定义t ,并返回t.id() By being a function with external linkage that instantiates thing<unsigned> , foo serves the purposes of:-通过作为一个具有外部链接的函数来实例化thing<unsigned>foo服务于:-

  • obliging the compiler to do that instantiating at all强制编译器完全实例化
  • exposing the instantiation in linkage so we can then probe what the linker does with it.在链接中公开实例化,这样我们就可以探测链接器对它做了什么。

Another source file:另一个源文件:

boo.cpp文件

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

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

which is just like foo.cpp except that it defines boo in place of foo and sets ID = 0xb00 .这就像foo.cpp除了它定义boo代替foo并设置ID = 0xb00

And lastly a program source:最后是一个程序源:

main.cpp主程序

#include <iostream>

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

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

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()的返回值 - 我们的作弊应该使 = f00 - 然后是boo()的返回值 - 我们的作弊应该使 = b00

Now we'll compile foo.cpp , and we'll do it with -save-temps because we want a look at the assembly:现在我们将编译foo.cpp ,我们将使用-save-temps因为我们想看看程序集:

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

This writes the assembly in foo.s and the portion of interest there is the definition of thing<unsigned int>::id() const (mangled = _ZNK5thingIjE2idEv ):这将程序集写入foo.s并且感兴趣的部分是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

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 (ie code) section of program in which the object file is linked.这将函数定义放在它自己称为.text._ZNK5thingIjE2idEv的链接部分中,如果需要,将输出,合并到目标文件链接的程序的.text (即代码)部分。 A linkage section like that, ie .text.<function_name> is called a function-section .像这样的链接部分,即.text.<function_name>被称为function-section It's a code section that contains only the definition of function <function_name> .这是一个代码部分,包含函数<function_name>的定义。

The directive:该指令:

.weak   _ZNK5thingIjE2idEv

is crucial.是至关重要的。 It classifies thing<unsigned int>::id() const as a weak symbol.它将thing<unsigned int>::id() const归类为符号。 The GNU linker recognises strong symbols and weak symbols. GNU 链接器识别符号和符号。 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.如果一个符号有多个弱定义而没有强定义,那么链接器可以任意选择任何一个弱定义。

The directive:该指令:

.type   _ZNK5thingIjE2idEv, @function

classifies thing<unsigned int>::id() as referring to a function - not data.thing<unsigned int>::id()为引用函数- 而不是数据。

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 .然后在定义的主体中,代码在由弱全局符号_ZNK5thingIjE2idEv标记的地址处组装,与局部标记为.LFB2的地址相同。 The code returns 3840 ( = 0xf00).代码返回 3840 (= 0xf00)。

Next we'll compile boo.cpp the same way:接下来我们将以同样的方式编译boo.cpp

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

and look again at how thing<unsigned int>::id() is defined in boo.s再看看thing<unsigned int>::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

It's identical, except for our cheat: this definition returns 2816 ( = 0xb00).它是相同的,除了我们的作弊:这个定义返回 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: -在这里,我们归结为:-

  • data数据
  • code代码
  • symbols, which can label data or label code.符号,可以标记数据或标记代码。

So nothing here specifically represents the instantiation of thing<T> for T = unsigned .所以这里没有什么特别代表thing<T>对于T = unsigned的实例化 All that's left of thing<unsigned> in this instance is the definition of _ZNK5thingIjE2idEv aka thing<unsigned int>::id() const .在这个例子中, thing<unsigned>剩下的thing<unsigned> _ZNK5thingIjE2idEv又名thing<unsigned int>::id() const

So now we know what the compiler does about instantiating thing<unsigned> in a given translation unit.所以现在我们知道编译器如何在给定的翻译单元中实例化thing<unsigned> 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.如果它必须实例化一个thing<unsigned>成员函数,那么它会将实例化成员函数的定义组装在一个标识该成员函数的弱全局符号上,并将这个定义放入它自己的函数部分中。

Now let's see what the linker does.现在让我们看看链接器做了什么。

First we'll compile the main source file.首先,我们将编译主源文件。

g++ -c main.cpp

Then link all the object files, requesting a diagnostic trace on _ZNK5thingIjE2idEv , and a linkage map file:然后链接所有目标文件,请求_ZNK5thingIjE2idEv上的诊断跟踪和链接映射文件:

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

So the linker tells us that the program gets the definition of _ZNK5thingIjE2idEv from foo.o and calls it in boo.o .所以链接器告诉我们程序从foo.o获取_ZNK5thingIjE2idEv的定义并在boo.o调用它。

Running the program shows it's telling the truth:运行该程序表明它说的是实话:

./prog

f00
f00

Both foo() and boo() are returning the value of thing<unsigned>().id() as instantiated in foo.cpp . foo()boo()都返回foo.cpp实例化thing<unsigned>().id()foo.cpp

What has become of the other definition of thing<unsigned int>::id() const in boo.o ? boo.othing<unsigned int>::id() const另一个定义是thing<unsigned int>::id() const The map file shows us:地图文件向我们展示了:

prog.map程序图

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

The linker chucked away the function-section in boo.o that contained the other definition.链接器boo.o了包含其他定义的boo.o中的函数部分。

Let's now link prog again, but this time with foo.o and boo.o in the reverse order:现在让我们再次链接prog ,但这次以相反的顺序使用foo.oboo.o

$ 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

This time, the program gets the definition of _ZNK5thingIjE2idEv from boo.o and calls it in foo.o .这一次,程序从boo.o获取_ZNK5thingIjE2idEv的定义,并在foo.o调用它。 The program confirms that:该程序确认:

$ ./prog

b00
b00

And the map file shows:地图文件显示:

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

that the linker chucked away the function-section .text._ZNK5thingIjE2idEv from foo.o .链接器从foo.o .text._ZNK5thingIjE2idEv了函数部分.text._ZNK5thingIjE2idEv

That completes the picture.这完成了图片。

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.多余的弱定义可以被链接器丢弃,而不会对程序造成附带损害,因为编译器将每个弱定义都单独放置在链接部分中。

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.但这很好,只要我们遵守跨多个翻译单元的 ODR ,因为我们这样做了,那么所有弱定义确实是相同的。 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.通常的做法是#include从头文件中的任何地方使用类模板(并且在我们这样做时不宏注入任何本地编辑)是遵守规则的一种相当健壮的方式。

Different implementations use different strategies for this.不同的实现为此使用不同的策略。

The GNU compiler, for example, marks template instantiations as weak symbols .例如,GNU 编译器将模板实例化标记为弱符号 Then at link time, the linker can throw away all definitions but one of the same weak symbol.然后在链接时,链接器可以丢弃所有定义,但相同的弱符号之一。

The Sun Solaris compiler, on the other hand, does not instantiate templates at all during normal compilation.另一方面,Sun Solaris 编译器在正常编译期间根本不实例化模板。 Then at link time, the linker collects all template instantiations needed to complete the program, and then goes ahead and calls the compiler in a special template-instantiation mode.然后在链接时,链接器收集完成程序所需的所有模板实例化,然后继续以特殊的模板实例化模式调用编译器。 Thus exactly one instantiation is produced for each template.因此,每个模板只生成一个实例。 There are no duplicates to merge or get rid of.没有要合并或删除的重复项。

Each approach has its own advantages and disadvantages.每种方法都有自己的优点和缺点。

When you have a non-template class definition, for example, class Bar {...};当您有一个非模板类定义时,例如class Bar {...}; , and this class is defined in the header, that is included in multiple translation units. , 并且这个类是在头文件中定义的,它包含在多个翻译单元中。 After compilation phase you have two object files with two definitions, right?在编译阶段之后,您有两个具有两个定义的目标文件,对吗? Do you think linker will create two binary definitions for the class in your final binary?您认为链接器会为最终二进制文件中的类创建两个二进制定义吗? Of course, you have two definitions in two translation units and one final definition in the final binary after the linkage phase is done.当然,在链接阶段完成后,您在两个翻译单元中有两个定义,在最终二进制文件中有一个最终定义。 This is called linkage collapsing, it is not forced by the standard, the standard only enforces the ODR rule , that does not say how the linker resolves the final problem, it is up to the linker, but the only way I have ever seen is the collapsing way of resolution.这称为链接折叠,它不是由标准强制,标准只强制执行ODR 规则,并没有说明链接器如何解决最终问题,这取决于链接器,但我见过的唯一方法是崩溃的解决方式。 Of course the linker can keep both definitions, but I cannot image why, since the standard enforces those definitions to be identical in their semantics (see the ODR rule link above for more details), and if those are not the program is ill-formed.当然,链接器可以保留这两个定义,但我无法想象为什么,因为标准强制这些定义在语义上是相同的(有关更多详细信息,请参阅上面的 ODR 规则链接),如果不是,则程序格式错误. Now imaging it was not Bar it was std::vector<int> .现在想象它不是Bar它是std::vector<int> Templates is just a way of code generation in this case, everything else is the same.在这种情况下,模板只是一种代码生成方式,其他一切都一样。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM