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

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

问题描述

假设我有两个翻译单元:

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.

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 的值是我们可以注入的跟踪器值.

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的外部链接,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.这是一个代码部分,包含函数的定义.

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::id() const 分类为 weak> 符号.GNU 链接器识别 符号和 符号.对于强符号,链接器将只接受链接中的一个定义.如果有更多,它将给出多个-定义错误.但是对于弱符号,它可以容忍任意数量的定义,并选择一个.如果一个弱定义的符号在链接中也有(只有一个)强定义,那么将选择强定义.如果一个符号有多个弱定义而没有强定义,然后链接器可以任意选择任何弱定义.

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 forT = 无符号.在这个例子中 thing 剩下的就是_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方面做了什么在给定的翻译单元中.如果必须实例化一个thing成员函数,然后组装实例化成员的定义在标识成员函数的弱全局符号处函数,并且它将此定义放入其自己的函数部分.

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() constother 定义变成了什么在 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.

到此完成图片.

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

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.

通过选择它看到的第一个弱定义,链接器有效地随机选择,因为目标文件的链接顺序是任意的.但这很好,只要我们遵守跨多个翻译单元的 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天全站免登陆