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

时间:2017-06-02 18:09:47

标签: c++ templates linker

假设我有两个翻译单元:

Foo.cpp中

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

bar.cpp

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

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

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

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

3 个答案:

答案 0 :(得分:18)

C ++ 要求 inline function definition 出现在引用该功能的翻译单元中。模板成员 函数是隐式内联的,但默认情况下也是用外部实例化的 连锁。因此,重复定义将在链接器可见时 相同的模板使用相同的模板参数进行实例化 翻译单位。链接器如何处理这种重复是你的问题。

您的C ++编译器受C ++标准的约束,但您的链接器不受主题限制 任何关于如何将C ++联系起来的成文标准:它本身就是一个法则, 植根于计算历史,对对象的源语言无动于衷 代码链接。您的编译器必须使用目标链接器 可以并且将会这样做,以便您可以成功链接您的程序并看到它们 你期待什么。所以我将向您展示GCC C ++编译器如何与之交互 GNU链接器用于处理不同转换单元中的相同模板实例化。

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

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

如果你怀疑这个作弊使演示无效,请记住:编译器 无法知道ODR是否永远尊重不同的翻译单位, 因此它在该帐户上的行为不同,并且没有这样的事情 作为&#34;作弊&#34;链接器。无论如何,演示将证明它是有效的。

首先我们有我们的作弊模板标题:

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

接下来是源文件:

<强> Foo.cpp中

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

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

它定义了foo所在的函数thing<unsigned> 实例化以定义t,并返回t.id()。通过作为一个功能 实例化thing<unsigned>的外部链接,foo用于此目的 的: -

  • 要求编译器完全实例化
  • 在连接中公开实例化,这样我们就可以探测出什么了 链接器用它做。

另一个源文件:

<强> boo.cpp

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

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

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

最后一个节目来源:

<强>的main.cpp

#include <iostream>

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

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

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

现在我们要编译foo.cpp,我们会-save-temps执行此操作,因为我们想要 看看大会:

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

这会将程序集写入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

顶部的三个指令很重要:

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

这个将函数定义放在自己调用的链接部分中 如果需要,将输出的.text._ZNK5thingIjE2idEv合并到。{} 表示目标文件链接的程序的.text(即代码)部分。一个 像这样的链接部分,即.text.<function_name>被称为功能部分。 它是一个代码部分,其中只包含 函数<function_name>的定义。

指令:

.weak   _ZNK5thingIjE2idEv

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

指令:

.type   _ZNK5thingIjE2idEv, @function

thing<unsigned int>::id()归类为引用功能 - 而不是数据。

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

接下来我们将以相同的方式编译boo.cpp

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

再次查看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

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

虽然我们在这里,但请注意一些可能会或可能不会说的内容: 一旦我们进入汇编(或目标代码),类就已经蒸发。这里, 我们归结为: -

  • 数据
  • 符号,可以标记数据或标签代码。

所以这里没有任何内容专门代表 thing<T> 的实例化 T = unsigned。在这个例子中thing<unsigned>左边的所有内容都是 _ZNK5thingIjE2idEv a.k.a thing<unsigned int>::id() const的定义。

现在我们知道编译器关于实例化thing<unsigned> 在给定的翻译单元中。如果有义务实例化thing<unsigned> 成员函数,然后它组装实例化成员的定义 在一个标识成员函数的弱全局符号中运行,以及它 将此定义放入其自己的函数部分。

现在让我们看看链接器的功能。

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

g++ -c main.cpp

然后链接所有目标文件,请求_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

所以链接器告诉我们程序从中获取_ZNK5thingIjE2idEv的定义 foo.o中的boo.o调用

运行程序显示它说实话:

./prog

f00
f00

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

thing<unsigned int>::id() const其他定义变成了什么 在boo.o?地图文件显示我们:

<强> prog.map

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

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

现在让我们再次链接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

这一次,该程序从_ZNK5thingIjE2idEvboo.o获得foo.o的定义 在$ ./prog b00 b00 中调用它。该计划确认:

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

地图文件显示:

.text._ZNK5thingIjE2idEv

链接器丢掉了功能部分foo.o 来自#include

完成图片。

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

通过选择它看到的第一个弱定义,链接器是有效的 随机选择,因为目标文件的链接顺序是任意的。 但这很好,只要我们遵守多个翻译单位的ODR , 因为我们这样做,所以所有的弱定义确实是相同的。通常的做法{{1}} - 从头文件中到处使用类模板(而不是在我们这样做的时候宏注入任何本地编辑)是一种遵循规则的相当强大的方法。

答案 1 :(得分:3)

不同的实现使用不同的策略。

例如,GNU编译器将模板实例化标记为weak symbols。然后在链接时,链接器可以丢弃所有定义,但是丢弃一个相同的弱符号。

另一方面,Sun Solaris编译器在正常编译期间根本不实例化模板。然后在链接时,链接器收集完成程序所需的所有模板实例化,然后继续并以特殊的模板实例化模式调用编译器。因此,为每个模板准确地生成一个实例。合并或摆脱没有重复。

每种方法都有其优点和缺点。

答案 2 :(得分:0)

如果您有非模板类定义,例如class Bar {...};,则此类在标题中定义,该类包含在多个翻译单元中。在编译阶段之后,你有两个有两个定义的目标文件,对吧?你认为链接器会在你的最终二进制文件中为类创建两个二进制定义吗?当然,在链接阶段完成后,您在两个翻译单元中有两个定义,在最终二进制文件中有一个最终定义。这称为链接折叠,它不是标准所强制的,标准只强制执行the ODR rule,没有说明链接器如何解决最终问题,它取决于链接器,但这是我唯一的方法看到的是崩溃的解决方式。当然链接器可以保留两个定义,但是我无法想象为什么,因为标准强制这些定义在语义上是相同的(有关更多详细信息,请参阅上面的ODR规则链接),如果不是,则程序不正确。现在成像不是Bar它是std::vector<int>。在这种情况下,模板只是代码生成的一种方式,其他一切都是相同的。