gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-02 06:53:44 26 4
gpt4 key购买 nike

假设我有两个翻译单元:

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/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com