問題描述
假設我有兩個翻譯單元:
Suppose I have two translation-units:
foo.cpp
void foo() {
auto v = std::vector<int>();
}
bar.cpp
void bar() {
auto v = std::vector<int>();
}
當我編譯這些翻譯單元時,每個單元都會實例化std::vector
.
When I compile these translation-units, each will instantiate std::vector<int>
.
我的問題是:這在鏈接階段是如何工作的?
My question is: how does this work at the linking stage?
- 兩個實例化的名稱是否不同?
- 鏈接器是否將它們作為重復項刪除?
推薦答案
C++ 要求內聯函數定義存在于引用該函數的翻譯單元中.模板成員函數是隱式內聯的,但默認情況下也使用外部實例化連鎖.因此,當鏈接器可見時,定義的重復同一個模板用不同的模板參數實例化翻譯單位.鏈接器如何處理這種重復是您的問題.
C++ requires that an inline function definition be present in a translation unit that references the function. Template member functions are implicitly inline, but also by default are instantiated with external linkage. Hence the duplication of definitions that will be visible to the linker when the same template is instantiated with the same template arguments in different translation units. How the linker copes with this duplication is your question.
您的 C++ 編譯器受 C++ 標準約束,但您的鏈接器不受約束任何關于如何鏈接 C++ 的編纂標準:它本身就是一條法律,植根于計算歷史,對對象的源語言漠不關心編碼它鏈接.您的編譯器必須使用目標鏈接器可以并且將會這樣做,以便您可以成功鏈接您的程序并查看它們你期望什么.所以我將向您展示 GCC C++ 編譯器如何與用于處理不同翻譯單元中相同模板實例的 GNU 鏈接器.
Your C++ compiler is subject to the C++ Standard, but your linker is not subject to any codified standard as to how it shall link C++: it is a law unto itself, rooted in computing history and indifferent to the source language of the object code it links. Your compiler has to work with what a target linker can and will do so that you can successfully link your programs and see them do what you expect. So I'll show you how the GCC C++ compiler interworks with the GNU linker to handle identical template instantiations in different translation units.
該演示利用了一個事實,即 C++ 標準 要求 -根據一個定義規則- 同一模板的不同翻譯單元中的實例化相同的模板參數應具有相同的定義,編譯器 -當然 - 不能對不同之間的關系強制執行任何類似的要求翻譯單位.它必須信任我們.
This demonstration exploits the fact that while the C++ Standard requires - by the One Definition Rule - that the instantiations in different translation units of the same template with the same template arguments shall have the same definition, the compiler - of course - cannot enforce any requirement like that on relationships between different translation units. It has to trust us.
所以我們會用不同的參數實例化同一個模板翻譯單元,但我們會通過將宏控制的差異注入到隨后將展示的不同翻譯單元中的實現我們鏈接器選擇的定義.
So we'll instantiate the same template with the same parameters in different translation units, but we'll cheat by injecting a macro-controlled difference into the implementations in different translation units that will subsequently show us which definition the linker picks.
如果您懷疑此作弊使演示無效,請記住:編譯器無法知道 ODR 是否曾經在不同的翻譯單元中受到尊重,所以它在那個帳戶上的行為不會有所不同,并且沒有這樣的事情作為欺騙"鏈接器.無論如何,演示將證明它是有效的.
If you suspect this cheat invalidates the demonstration, remember: the compiler cannot know whether the ODR is ever honoured across different translation units, so it cannot behave differently on that account, and there's no such thing as "cheating" the linker. Anyhow, the demo will demonstrate that it is valid.
首先我們有我們的作弊模板標題:
First we have our cheat template header:
thing.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
的值是我們可以注入的跟蹤器值.
The value of the macro ID
is the tracer value we can inject.
下一個源文件:
foo.cpp
#define ID 0xf00
#include "thing.hpp"
unsigned foo()
{
thing<unsigned> t;
return t.id();
}
它定義了函數foo
,其中thing
是實例化定義t
,返回t.id()
.通過成為一個函數實例化thing
的外部鏈接,foo
服務于目的的:-
It defines function foo
, in which thing<unsigned>
is
instantiated to define t
, and t.id()
is returned. By being a function with
external linkage that instantiates thing<unsigned>
, foo
serves the purposes
of:-
- 強制編譯器完全實例化
- 在鏈接中公開實例化,以便我們可以探查鏈接器會處理它.
另一個源文件:
boo.cpp
#define ID 0xb00
#include "thing.hpp"
unsigned boo()
{
thing<unsigned> t;
return t.id();
}
和 foo.cpp
一樣,只是它定義了 boo
代替了 foo
和設置 ID
= 0xb00
.
which is just like foo.cpp
except that it defines boo
in place of foo
and
sets ID
= 0xb00
.
最后一個程序源:
ma??in.cpp
#include <iostream>
extern unsigned foo();
extern unsigned boo();
int main()
{
std::cout << std::hex
<< '
' << foo()
<< '
' << boo()
<< std::endl;
return 0;
}
這個程序將以十六進制打印foo()
的返回值——我們的作弊者應該這樣做= f00
- 然后是 boo()
的返回值 - 我們的作弊應該使 = b00
.
This program will print, as hex, the return value of foo()
- which our cheat should make
= f00
- then the return value of boo()
- which our cheat should make = b00
.
現在我們將編譯 foo.cpp
,我們將使用 -save-temps
來完成,因為我們想要看看程序集:
Now we'll compile foo.cpp
, and we'll do it with -save-temps
because we want
a look at the assembly:
g++ -c -save-temps foo.cpp
這會在 foo.s
中編寫程序集,感興趣的部分是thing
的定義(mangled = _ZNK5thingIjE2idEv
):
This writes the assembly in foo.s
and the portion of interest there is
the definition of thing<unsigned int>::id() const
(mangled = _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
頂部的三個指令很重要:
Three of the directives at the top are significant:
.section .text._ZNK5thingIjE2idEv,"axG",@progbits,_ZNK5thingIjE2idEv,comdat
這個將函數定義放在它自己稱為的鏈接部分中.text._ZNK5thingIjE2idEv
將輸出,如果需要,合并到.text
(即代碼)目標文件鏈接的程序部分.一個像這樣的鏈接部分,即 .text.
被稱為 function-section.這是一個代碼部分,僅包含函數
的定義.
This one puts the function definition in a linkage section of its own called
.text._ZNK5thingIjE2idEv
that will be output, if it's needed, merged into the
.text
(i.e. code) section of program in which the object file is linked. A
linkage section like that, i.e. .text.<function_name>
is called a function-section.
It's a code section that contains only the definition of function <function_name>
.
指令:
.weak _ZNK5thingIjE2idEv
至關重要.它將 thing
分類為 weak> 符號.GNU 鏈接器識別強 符號和弱 符號.對于強符號,鏈接器將只接受鏈接中的一個定義.如果有更多,它將給出多個-定義錯誤.但是對于弱符號,它可以容忍任意數量的定義,并選擇一個.如果一個弱定義的符號在鏈接中也有(只有一個)強定義,那么將選擇強定義.如果一個符號有多個弱定義而沒有強定義,然后鏈接器可以任意選擇任何弱定義.
is crucial. It classifies thing<unsigned int>::id() const
as a weak symbol.
The GNU linker recognises strong symbols and weak symbols. For a strong symbol, the
linker will accept only one definition in the linkage. If there are more, it will give a multiple
-definition error. But for a weak symbol, it will tolerate any number of definitions,
and pick one. If a weakly defined symbol also has (just one) strong definition in the linkage then the
strong definition will be picked. If a symbol has multiple weak definitions and no strong definition,
then the linker can pick any one of the weak definitions, arbitrarily.
指令:
.type _ZNK5thingIjE2idEv, @function
將thing
歸類為引用函數 - 而不是數據.
classifies thing<unsigned int>::id()
as referring to a function - not data.
然后在定義體中,在地址處組裝代碼由弱全局符號_ZNK5thingIjE2idEv
標記,本地相同標記為 .LFB2
.代碼返回 3840 (= 0xf00).
Then in the body of the definition, the code is assembled at the address
labelled by the weak global symbol _ZNK5thingIjE2idEv
, the same one locally
labelled .LFB2
. The code returns 3840 ( = 0xf00).
接下來我們將以同樣的方式編譯boo.cpp
:
Next we'll compile boo.cpp
the same way:
g++ -c -save-temps boo.cpp
再看看 thing
在 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).
It's identical, except for our cheat: this definition returns 2816 ( = 0xb00).
當我們在這里時,讓我們注意一些可能不言而喻的事情:一旦我們進入匯編(或目標代碼),類就消失了.這里,我們的目標是:-
While we're here, let's note something that might or might not go without saying: Once we're in assembly (or object code), classes have evaporated. Here, we're down to: -
- 數據
- 代碼
- 符號,可以標記數據或標記代碼.
所以這里沒有什么特別代表的實例化thing
forT = 無符號
.在這個例子中 thing
剩下的就是_ZNK5thingIjE2idEv
又名 thing
的定義.
So nothing here specifically represents the instantiation of thing<T>
for
T = unsigned
. All that's left of thing<unsigned>
in this instance is
the definition of _ZNK5thingIjE2idEv
a.k.a thing<unsigned int>::id() const
.
現在我們知道編譯器在實例化thing
方面做了什么在給定的翻譯單元中.如果必須實例化一個thing
成員函數,然后組裝實例化成員的定義在標識成員函數的弱全局符號處函數,并且它將此定義放入其自己的函數部分.
So now we know what the compiler does about instantiating thing<unsigned>
in a given translation unit. If it is obliged to instantiate a thing<unsigned>
member function, then it assembles the definition of the instantiated member
function at a weakly global symbol that identifies the member function, and it
puts this definition into its own function-section.
現在讓我們看看鏈接器做了什么.
Now let's see what the linker does.
首先我們將編譯主源文件.
First we'll compile the main source file.
g++ -c main.cpp
然后鏈接所有目標文件,請求對 _ZNK5thingIjE2idEv
進行診斷跟蹤,和一個鏈接映射文件:
Then link all the object files, requesting a diagnostic trace on _ZNK5thingIjE2idEv
,
and a linkage map file:
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
所以鏈接器告訴我們程序從foo.o
并在 boo.o
中調用.
So the linker tells us that the program gets the definition of _ZNK5thingIjE2idEv
from
foo.o
and calls it in boo.o
.
運行該程序表明它說的是實話:
Running the program shows it's telling the truth:
./prog
f00
f00
foo()
和 boo()
都返回 thing
的值在 foo.cpp
中實例化.
Both foo()
and boo()
are returning the value of thing<unsigned>().id()
as instantiated in foo.cpp
.
thing
的 other 定義變成了什么在 boo.o
中?地圖文件向我們展示了:
What has become of the other definition of thing<unsigned int>::id() const
in boo.o
? The map file shows us:
prog.map
...
Discarded input sections
...
...
.text._ZNK5thingIjE2idEv
0x0000000000000000 0xf boo.o
...
...
鏈接器刪除了 boo.o
中的函數部分包含另一個定義.
The linker chucked away the function-section in boo.o
that
contained the other definition.
現在讓我們再次鏈接 prog
,但這次使用 foo.o
和 boo.o
在倒序:
Let's now link prog
again, but this time with foo.o
and boo.o
in the
reverse order:
$ 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
的定義,然后在 foo.o
中調用它.程序確認:
This time, the program gets the definition of _ZNK5thingIjE2idEv
from boo.o
and
calls it in foo.o
. The program confirms that:
$ ./prog
b00
b00
地圖文件顯示:
...
Discarded input sections
...
...
.text._ZNK5thingIjE2idEv
0x0000000000000000 0xf foo.o
...
...
鏈接器丟棄了函數部分 .text._ZNK5thingIjE2idEv
來自 foo.o
.
that the linker chucked away the function-section .text._ZNK5thingIjE2idEv
from foo.o
.
到此完成圖片.
編譯器在每個翻譯單元中發出一個弱定義每個實例化的模板成員都在其自己的函數部分中.鏈接器然后只選擇它遇到的那些弱定義的第一個在需要解析弱引用的鏈接序列中象征.因為每個弱符號都指向一個定義,所以任何其中之一 - 特別是第一個 - 可用于解析所有引用到鏈接中的符號,其余的弱定義是消耗品.多余的弱定義必須被忽略,因為鏈接器只能鏈接給定符號的一個定義.還有多余的弱定義可以被鏈接器丟棄,無需任何擔保程序損壞,因為編譯器將每一個都單獨放置在一個鏈接部分中.
The compiler emits, in each translation unit, a weak definition of each instantiated template member in its own function section. The linker then just picks the first of those weak definitions that it encounters in the linkage sequence when it needs to resolve a reference to the weak symbol. Because each of the weak symbols addresses a definition, any one one of them - in particular, the first one - can be used to resolve all references to the symbol in the linkage, and the rest of the weak definitions are expendable. The surplus weak definitions must be ignored, because the linker can only link one definition of a given symbol. And the surplus weak definitions can be discarded by the linker, with no collateral damage to the program, because the compiler placed each one in a linkage section all by itself.
通過選擇它看到的第一個弱定義,鏈接器有效地隨機選擇,因為目標文件的鏈接順序是任意的.但這很好,只要我們遵守跨多個翻譯單元的 ODR,因為我們這樣做了,那么所有弱定義確實是相同的.#include
的通常做法是從頭文件中的任何地方使用類模板(并且在我們這樣做時不宏注入任何本地編輯)是遵守規則的一種相當可靠的方式.
By picking the first weak definition it sees, the linker is effectively
picking at random, because the order in which object files are linked is arbitrary.
But this is fine, as long as we obey the ODR accross multiple translation units,
because it we do, then all of the weak definitions are indeed identical. The usual practice of #include
-ing a class template everywhere from a header file (and not macro-injecting any local edits when we do so) is a fairly robust way of obeying the rule.
這篇關于鏈接器如何處理跨翻譯單元的相同模板實例化?的文章就介紹到這了,希望我們推薦的答案對大家有所幫助,也希望大家多多支持html5模板網!