链接器如何处理跨翻译单元的相同模板实例化
How does the linker handle identical template instantiations across translation units?
假设我有两个翻译单元:
傅.cpp
void foo() {
auto v = std::vector<int>();
}
酒吧.cpp
void bar() {
auto v = std::vector<int>();
}
当我编译这些翻译单元时,每个单元都会实例化std::vector<int>
.
我的问题是:这在链接阶段是如何工作的?
- 两个实例是否具有不同的损坏名称?
- 链接器是否将它们作为重复项删除?
C++要求内联函数定义 出现在引用函数的翻译单元中。模板成员 函数是隐式内联的,但默认情况下也使用外部实例化 联动。因此,链接器在以下情况下可以看到的重复定义 同一模板在不同的模板参数中实例化 翻译单位。链接器如何处理这种重复是您的问题。
C++编译器受C++标准约束,但链接器不受约束 关于它如何与C++联系起来的任何成文标准:它本身就是一项法律, 植根于计算历史,对对象的源语言漠不关心 编码它链接。您的编译器必须使用目标链接器 可以并且将这样做,以便您可以成功链接您的程序并看到它们这样做 您的期望。因此,我将向您展示GCC C++编译器如何与 GNU 链接器,用于处理不同翻译单元中的相同模板实例化。
本演示利用了这样一个事实,即虽然C++标准要求- 通过一个定义规则 - 同一模板的不同翻译单元中的实例化 相同的模板参数应具有相同的定义,编译器 - 当然 - 不能对不同之间的关系强制执行任何这样的要求 翻译单位。它必须信任我们。
因此,我们将在不同的模板中使用相同的参数实例化相同的模板 翻译单元,但我们会通过将宏观控制的差异注入到中来作弊 随后将显示的不同翻译单元中的实现 链接器选择哪个定义。
如果您怀疑此作弊使演示无效,请记住:编译器 无法知道 ODR 是否在不同翻译单位中得到遵守, 所以它不能在这个帐户上表现不同,也没有这样的事情 作为"欺骗"链接器。无论如何,演示将证明它是有效的。
首先,我们有作弊模板标头:
东西.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
的值是我们可以注入的示踪剂值。
接下来是源文件:
傅.cpp
#define ID 0xf00
#include "thing.hpp"
unsigned foo()
{
thing<unsigned> t;
return t.id();
}
它定义了函数foo
,其中thing<unsigned>
是 实例化以定义t
,并返回t.id()
。通过成为具有以下功能的函数 实例化thing<unsigned>
的外部链接,foo
服务于目的 之:-
- 强制编译器完全实例化
- 在链接中公开实例化,以便我们可以探测 链接器用它做。
另一个源文件:
嘭.cpp
#define ID 0xb00
#include "thing.hpp"
unsigned boo()
{
thing<unsigned> t;
return t.id();
}
这就像foo.cpp
一样,只是它定义了boo
代替foo
和 集合ID
=0xb00
。
最后是程序源:
主.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
的定义(损坏 =_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
归类为弱符号。 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)。
当我们在这里时,让我们注意一些可能不言而喻也可能不言而喻的事情: 一旦我们进入汇编(或目标代码),类就消失了。这里 我们归结为: -
- 数据
- 法典
- 符号,可以标记数据或标记代码。
所以这里没有什么具体代表T = unsigned
的thing<T>
实例化。在这种情况下,thing<unsigned>
剩下的就是_ZNK5thingIjE2idEv
又名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
?地图文件向我们展示了:
进度地图
...
Discarded input sections
...
...
.text._ZNK5thingIjE2idEv
0x0000000000000000 0xf boo.o
...
...
链接器删除了功能部分,boo.o
包含另一个定义。
现在让我们再次链接prog
,但这次是foo.o
和boo.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
这一次,程序_ZNK5thingIjE2idEv
从boo.o
和 用foo.o
称呼它。该计划确认:
$ ./prog
b00
b00
地图文件显示:
...
Discarded input sections
...
...
.text._ZNK5thingIjE2idEv
0x0000000000000000 0xf foo.o
...
...
链接器删除了功能部分.text._ZNK5thingIjE2idEv
从foo.o
.
这幅画就完成了。
编译器在每个翻译单元中发出一个弱定义 每个实例化的模板成员都位于其自己的函数部分中。链接器 然后只选择它遇到的第一个弱定义 在需要解决对弱者的引用时的链接序列中 象征。由于每个弱符号都涉及一个定义,因此任何 其中一个 - 特别是第一个 - 可用于解析所有引用 到链接中的符号,其余的弱定义是 消耗品。必须忽略多余的弱定义,因为 链接器只能链接给定符号的一个定义。和盈余 弱定义可以被链接器丢弃,没有抵押品 损坏程序,因为编译器将每个单独放置在链接部分中。
通过选择它看到的第一个弱定义,链接器有效地 随机选取,因为对象文件的链接顺序是任意的。 但这很好,只要我们遵守多个翻译单元的 ODR, 因为我们这样做,那么所有弱定义确实是相同的。通常的做法是从头文件的所有位置#include
类模板(并且在我们这样做时不宏注入任何本地编辑)是一种相当健壮的遵守规则的方式。
不同的实现为此使用不同的策略。
例如,GNU 编译器将模板实例化标记为弱符号。然后在链接时,链接器可以抛弃除除相同弱符号之一之外的所有定义。
另一方面,Sun Solaris 编译器在正常编译期间根本不实例化模板。然后在链接时,链接器收集完成程序所需的所有模板实例化,然后继续以特殊的模板实例化模式调用编译器。因此,为每个模板只生成一个实例化。没有要合并或删除的重复项。
每种方法都有自己的优点和缺点。
当您有一个非模板类定义时,例如,class Bar {...};
,并且此类在标头中定义,包含在多个翻译单元中。在编译阶段之后,您有两个具有两个定义的目标文件,对吗?你认为链接器会在最终的二进制文件中为类创建两个二进制定义吗?当然,在链接阶段完成后,您在两个翻译单元中有两个定义,在最终二进制文件中有一个最终定义。这叫链接折叠,它不是标准强制的,标准只执行ODR规则,那没有说链接器如何解决最终问题,这取决于链接器,但我见过的唯一方法是折叠的解决方法。当然,链接器可以保留这两个定义,但我无法想象为什么,因为标准强制这些定义在语义上相同(有关更多详细信息,请参阅上面的 ODR 规则链接),如果不是,程序格式不正确。现在成像它不是Bar
它是std::vector<int>
.在这种情况下,模板只是代码生成的一种方式,其他一切都是一样的。
- 从C++实例化QML
- 设计一个只能由特定类实例化的类(如果可能的话,通过make_unique)
- 如何创建一个空的全局类并在启动时实例化它
- 在两个类中共享相同的函数调用,并在不需要时避免空实例化
- 约束和显式模板实例化
- 为什么包含windows.h会产生语法错误,从而阻止类的实例化?(C2146,C2065)
- 对象实例化调用构造函数的次数太多
- 如何使用非默认构造函数实例化模板化类
- 静态数据成员模板专用化的实例化点在哪里
- 错误的cv::face FacemarkLBF实例化
- C++的解析器在可以区分比较和模板实例化之前会做什么?
- 为什么 gcc 和 clang 为函数模板的实例化生成不同的符号名称?
- 检查某些类型是否是模板类 std::optional 的实例化
- 链接器如何处理跨翻译单元的相同模板实例化
- 使用不同优化级别编译的不同翻译单元中的模板实例化
- 实例化一个二维向量,其中一组对作为单元值
- 在多个翻译单元中函数模板实例化的标识
- 跨编译单元的相同函数模板实例化的地址
- 实例化的点是否可以延迟到翻译单元结束
- 使用静态char*数组成员的模板特化,在多个单元中实例化