假设我有两个翻译单元:
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.o
和foo()
都返回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
,但这一次是将prog
和foo.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/