c++ - 链接器如何在翻译单元之间处理相同的模板实例化?

标签 c++ templates linker

假设我有两个翻译单元:

foo.cpp

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

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

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

我的问题是:这在链接阶段如何工作?
  • 两种实例化名称是否都有不同?
  • 链接器是否将它们作为重复项删除?
  • 最佳答案

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

    您的C++编译器受C++标准的约束,但您的链接器不受此约束
    关于如何将C++链接起来的任何成文标准:这是它本身的法律,
    Root 于计算历史,对对象的源语言无动于衷
    对其链接进行编码。您的编译器必须使用目标链接器
    可以并且将会这样做,以便您可以成功地链接程序并查看它们
    您的期望。因此,我将向您展示GCC C++编译器如何与
    GNU链接器以不同的翻译单元处理相同的模板实例。

    该演示利用了以下事实:尽管C++标准要求-
    One Definition Rule
    -同一模板的不同翻译单元中的实例化具有
    相同的模板参数应具有相同的定义,编译器-
    当然-不能对不同的关系强制执行类似的要求
    翻译单位。它必须信任我们。

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

    如果您怀疑此作弊使演示无效,请记住:编译器
    不知道ODR是否曾经在不同的翻译部门得到认可,
    因此它在该帐户上的行为不会有所不同,并且没有这样的事情
    作为“欺骗”链接器。无论如何,该演示将证明它是有效的。

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

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

    接下来是一个源文件:

    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

    最后是一个程序源:

    main.cpp
    #include <iostream>
    
    extern unsigned foo();
    extern unsigned boo();
    
    int main()
    {
        std::cout << std::hex 
        << '\n' << foo()
        << '\n' << boo()
        << std::endl;
        return 0;
    }
    

    该程序将以十六进制形式打印0xb00的返回值-我们的作弊者应使用此返回值
    = foo()-然后是f00的返回值-我们的作弊应该使它成为= boo()

    现在,我们将编译b00,并使用foo.cpp进行编译,因为我们想要
    看看组装:
    g++ -c -save-temps foo.cpp
    

    这将程序集写入-save-temps中,感兴趣的部分是foo.s的定义(mangled = thing<unsigned int>::id() const):
        .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
    

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

    指令:
    .weak   _ZNK5thingIjE2idEv
    

    至关重要。它将<function_name>归类为weak符号。
    GNU链接器可识别强符号和弱符号。要获得强烈的象征,
    链接器在链接中仅接受一个定义。如果还有更多,它将给出倍数
    -定义错误。但是对于弱符号,它可以容忍任何数量的定义,
    选一个如果一个弱定义的符号在链接中也具有(仅一个)强定义,则
    会选择强定义。如果符号具有多个弱定义而没有强定义,
    则链接器可以任意选择任何一个弱定义。

    指令:
    .type   _ZNK5thingIjE2idEv, @function
    

    thing<unsigned int>::id() const分类为引用函数-而不是数据。

    然后在定义主体中,将代码汇编到该地址处
    用弱全局符号thing<unsigned int>::id()标记,本地相同
    标记为_ZNK5thingIjE2idEv。该代码返回3840(= 0xf00)。

    接下来,我们将以相同的方式编译.LFB2:
    g++ -c -save-temps boo.cpp
    

    再看看boo.cpp中如何定义thing<unsigned int>::id()
        .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)。

    当我们在这里时,让我们注意一些可能会或可能不会发生的事情:
    一旦我们进行汇编(或目标代码),类就会消失。这里,
    我们归结为:-
  • 数据
  • 代码
  • 符号,可以标记数据或标记代码。

  • 因此,这里没有任何内容专门代表boo.s的实例化thing<T>。在这种情况下,T = unsigned剩下的全部是thing<unsigned>的定义,也称为_ZNK5thingIjE2idEv

    现在我们知道了编译器如何实例化thing<unsigned int>::id() const在给定的翻译单元中。如果必须实例化thing<unsigned>成员函数,然后组装实例化成员的定义
    在标识成员函数的弱全局符号处起作用,并且它
    将此定义放入其自己的功能部分。

    现在,让我们看看链接器的作用。

    首先,我们将编译主要的源文件。
    g++ -c main.cpp
    

    然后链接所有目标文件,请求对thing<unsigned>进行诊断跟踪,
    和链接映射文件:
    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的定义:_ZNK5thingIjE2idEv并在foo.o中调用它。

    运行程序表明它说的是实话:
    ./prog
    
    f00
    f00
    
    boo.ofoo()都返回boo()的值
    thing<unsigned>().id()中实例化的。
    foo.cpp的其他定义已成为什么
    thing<unsigned int>::id() const中?该 map 文件显示了我们:

    程序 map
    ...
    Discarded input sections
     ...
     ...
     .text._ZNK5thingIjE2idEv
                    0x0000000000000000        0xf boo.o
     ...
     ...
    

    链接器删除了boo.o中的功能部分,该部分
    包含另一个定义。

    现在,让我们再次链接boo.o,但这一次是将progfoo.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
    

    这次,程序从boo.o获取_ZNK5thingIjE2idEv的定义,并
    将其称为boo.o。该程序确认:
    $ ./prog
    
    b00
    b00
    

    map 文件显示:
    ...
    Discarded input sections
     ...
     ...
     .text._ZNK5thingIjE2idEv
                    0x0000000000000000        0xf foo.o
     ...
     ...
    

    链接器删除了功能部分foo.o来自.text._ZNK5thingIjE2idEv

    这样就完成了。

    编译器在每个翻译单元中发出一个弱定义:
    每个实例化的模板成员都位于其自己的功能部分。链接器
    然后只选择它遇到的那些弱定义中的第一个
    在链接序列中需要解决对弱点的引用时
    符号。因为每个弱符号都涉及一个定义,所以任何
    其中一个-尤其是第一个-可用于解析所有引用
    链接中的符号,其余的弱定义是
    消耗的。多余的弱定义必须忽略,因为
    链接器只能链接给定符号的一个定义。还有盈余
    链接器可以舍弃弱定义,而无需任何抵押
    损害程序,因为编译器本身将每个部分放置在一个链接部分中。

    通过选择看到的第一个弱定义,链接器可以有效地
    随机选择,因为链接目标文件的顺序是任意的。
    但这很好,只要我们遵守跨多个翻译部门的ODR,
    因为我们做到了,所以所有的弱定义的确是相同的。 foo.o的通常做法是在头文件中的任何地方放置类模板(并且这样做时不进行宏注入(inject)任何本地编辑)是一种遵循规则的相当可靠的方法。

    关于c++ - 链接器如何在翻译单元之间处理相同的模板实例化?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44335046/

    相关文章:

    c++ - 如何在类主体中调用模板函数?

    linux - 为什么ldconfig能够找到一个库,但是找不到Rust?

    makefile - CUDA 8.0 nvcc致命: single input file required for a non-link phase when an ouputtfile is specified

    c++ - 多态显式模板实例化

    c++ - 具有非数据类型模板参数的类模板特化

    c++ - virtual对类模板成员使用的影响

    c - 动态链接错误

    c++ - 我在哪里可以看到 mfc 应用程序中的 printf 输出?

    c++ - 在 qt 中切换编译器后出现错误