C++ requires那一个内联函数定义 http://en.cppreference.com/w/cpp/language/inline存在于引用该函数的翻译单元中。模板成员
函数是隐式内联的,但默认情况下也使用外部实例化
连锁。因此,当以下情况时,链接器将看到重复的定义:
相同的模板在不同的环境中使用相同的模板参数进行实例化
翻译单位。链接器如何处理这种重复是你的问题。
您的 C++ 编译器受 C++ 标准约束,但您的链接器不受约束
关于如何链接 C++ 的任何编纂标准:它本身就是一条法则,
植根于计算历史,与对象的源语言无关
对其链接进行编码。您的编译器必须与目标链接器一起工作
可以并且将会这样做,以便您可以成功链接您的程序并查看它们的情况
你所期望的。所以我将向您展示 GCC C++ 编译器如何与
GNU 链接器处理不同翻译单元中相同的模板实例化。
该演示利用了这样一个事实:虽然 C++ 标准requires-
由一种定义规则 http://en.cppreference.com/w/cpp/language/definition- 同一模板的不同翻译单元中的实例化
相同的模板参数应具有相同的定义,编译器 -
当然 - 不能对不同人之间的关系强制执行任何这样的要求
翻译单位。它必须信任我们。
因此,我们将在不同的环境中使用相同的参数实例化相同的模板
翻译单元,但我们会通过将宏观控制的差异注入到
随后将显示不同翻译单元中的实现
我们链接器选择哪个定义。
如果您怀疑此作弊会使演示无效,请记住:编译器
无法知道 ODR 是否是ever荣获不同翻译单位的荣誉,
所以它在这个帐户上的行为不会有所不同,而且不存在这样的事情
作为“欺骗”链接器。无论如何,演示将证明它是有效的。
首先我们有我们的作弊模板标题:
东西.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
, and 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
(损坏=_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
(即代码)链接目标文件的程序部分。 A
像这样的链接部分,即.text.<function_name>
被称为功能部分。
这是一个包含以下内容的代码部分only函数的定义<function_name>
.
该指令:
.weak _ZNK5thingIjE2idEv
至关重要。它分类thing<unsigned int>::id() const
as a weak https://en.wikipedia.org/wiki/Weak_symbol象征。
GNU 链接器识别strong符号和weak符号。对于一个强符号来说,
链接器仅接受链接中的一个定义。如果有更多,它将给出倍数
- 定义错误。但对于弱符号来说,它可以容忍任意数量的定义,
并选择一个。如果一个弱定义的符号在链接中也有(只有一个)强定义,那么
将选择强定义。如果一个符号有多个弱定义且没有强定义,
然后链接器可以选择any one任意的弱定义。
该指令:
.type _ZNK5thingIjE2idEv, @function
分类thing<unsigned int>::id()
指的是function- 不是数据。
然后在定义体中,代码在地址处汇编
由弱全局符号标记_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)。
当我们在这里时,让我们注意一些可能不言而喻的事情:
一旦我们进入汇编(或目标代码),班级已经消失。这里,
我们要做的是:-
- data
- code
- 符号,可以标记数据或标记代码。
所以这里没有具体代表的实例化 thing<T>
for
T = unsigned
。剩下的一切thing<unsigned>
在这种情况下是
的定义_ZNK5thingIjE2idEv
a.k.a thing<unsigned int>::id() const
.
所以现在我们知道什么是compiler关于实例化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
from
foo.o
and calls it in boo.o
.
运行该程序表明它说的是实话:
./prog
f00
f00
Both foo()
and boo()
正在返回的值thing<unsigned>().id()
实例化于 foo.cpp
.
变成了什么样子other的定义thing<unsigned int>::id() const
in boo.o
?地图文件向我们展示了:
prog.map
...
Discarded input sections
...
...
.text._ZNK5thingIjE2idEv
0x0000000000000000 0xf boo.o
...
...
链接器丢弃了函数部分boo.o
那
包含其他定义。
现在让我们链接prog
再次,但这次与foo.o
and 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
from boo.o
和
调用它foo.o
。该计划确认:
$ ./prog
b00
b00
地图文件显示:
...
Discarded input sections
...
...
.text._ZNK5thingIjE2idEv
0x0000000000000000 0xf foo.o
...
...
链接器丢弃了函数部分.text._ZNK5thingIjE2idEv
from foo.o
.
这样就完成了图片。
编译器在每个翻译单元中发出一个弱定义
每个实例化的模板成员都在其自己的函数部分中。链接器
然后只需选择first它遇到的那些弱定义
在链接序列中,当需要解析对弱的引用时
象征。因为每个弱符号都涉及一个定义,所以任何
其中一个 - 特别是第一个 - 可用于解析所有引用
到链接中的符号,其余的弱定义是
消耗品。多余的弱定义必须被忽略,因为
链接器只能链接给定符号的一个定义。还有剩余的
弱定义可以是丢弃的由链接器提供,无需抵押品
对程序造成损害,因为编译器将每个链接单独放置在链接节中。
通过选择first它看到的弱定义,链接器是有效的
随机选择,因为目标文件的链接顺序是任意的。
不过这样也好,只要我们遵守多个翻译单位的命令,
因为我们这样做了,那么所有的弱定义确实是相同的。通常的做法是#include
- 从头文件中的任何地方使用类模板(并且这样做时不宏注入任何本地编辑)是遵守规则的相当可靠的方法。