链接器如何允许在不同目标文件中定义多个函数模板,但只允许定义一个普通函数

How linker allow multiple definitions of a function template in different object files but only allow one-definition of ordinary functions

我知道在使用 C++ 模板时如何使用内联关键字来避免 'multiple definition'。不过,我很好奇的是,linker是如何区分哪个特化是全特化违反ODR报错,而另一个特化是隐式并正确处理的呢?

nm 输出中,我们可以看到 main.o 和 other.o 中对 int-version max() 和 char-version max() 的重复定义,但是 C++ linker 只报告 'multiple definition error for char-version max()' 但让 'char-version max() 成功 link? linker 如何区分它们并做到这一点?

// tmplhdr.hpp
#include <iostream>

// this function is instantiated in main.o and other.o
// but leads no 'multiple definition' error by linker
template<typename T>
T max(T a, T b)
{
    std::cout << "match generic\n";
    return (b<a)?a:b;
}

// 'multiple definition' link error if without inline
template<>
inline char max(char a, char b)
{
    std::cout << "match full specialization\n";
    return (b<a)?a:b;
}
// main.cpp
#include "tmplhdr.hpp"

extern int mymax(int, int);

int main()
{
    std::cout << max(1,2) << std::endl;
    std::cout << mymax(10,20) << std::endl;
    std::cout << max('a','b') << std::endl;
    return 0;
}
// other.cpp
#include "tmplhdr.hpp"

int mymax(int a, int b)
{
    return max(a, b);
}

Ubuntu 上的测试输出是合理的;但是 Cygwin 上的输出相当奇怪和令人困惑...

==== 在 Cygwin 上测试 ====

g++ linker 只报告 'char max(char, char)' 是重复的。

$ g++ -o main.exe main.cpp other.cpp
/usr/lib/gcc/x86_64-pc-cygwin/11/../../../../x86_64-pc-cygwin/bin/ld: 
/tmp/ccYivs3O.o:other.cpp:(.text$_Z3maxIcET_S0_S0_[_Z3maxIcET_S0_S0_]+0x0): 
multiple definition of `char max<char>(char, char)'; 
/tmp/cc7HJqbS.o:main.cpp:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status

我转储了我的 .o 目标文件,但没有找到太多线索(可能我对目标格式规范不太熟悉)。

$ nm main.o | grep max | c++filt.exe
0000000000000000 p .pdata$_Z3maxIcET_S0_S0_
0000000000000000 p .pdata$_Z3maxIiET_S0_S0_
0000000000000000 t .text$_Z3maxIcET_S0_S0_
0000000000000000 t .text$_Z3maxIiET_S0_S0_
0000000000000000 r .xdata$_Z3maxIcET_S0_S0_
0000000000000000 r .xdata$_Z3maxIiET_S0_S0_
0000000000000000 T char max<char>(char, char) <-- full specialization
0000000000000000 T int max<int>(int, int) <<-- implicit specialization
                 U mymax(int, int)
$ nm other.o | grep max | c++filt.exe
0000000000000000 p .pdata$_Z3maxIcET_S0_S0_
0000000000000000 p .pdata$_Z3maxIiET_S0_S0_
0000000000000000 t .text$_Z3maxIcET_S0_S0_
0000000000000000 t .text$_Z3maxIiET_S0_S0_
0000000000000000 r .xdata$_Z3maxIcET_S0_S0_
0000000000000000 r .xdata$_Z3maxIiET_S0_S0_
000000000000009b t _GLOBAL__sub_I__Z5mymaxii
0000000000000000 T char max<char>(char, char) <-- full specialization
0000000000000000 T int max<int>(int, int) <-- implicit specialization
0000000000000000 T mymax(int, int)

==== 测试 Ubuntu ====

这是我从 tmplhdr.hpp

中删除 inline 后用 g++-9 在 Ubuntu 上得到的结果
tony@Win10Bedroom:/mnt/c/Users/Tony Su/My Documents/cpphome$ g++ -o main main.o other.o
/usr/bin/ld: other.o: in function `char max<char>(char, char)':
other.cpp:(.text+0x0): multiple definition of `char max<char>(char, char)'; main.o:main.cpp:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status

'char-version max()'标记为T,不允许有多个定义;但是 'in-version max()' 被标记为 W 允许多个定义。但是,我开始好奇为什么 nm 在 Cygwin 上给出的标记与在 Ubuntu 上给出的标记不同?为什么 Cgywin 上的 linker 可以正确处理两个 T 定义?

tony@Win10Bedroom:/mnt/c/Users/Tony Su/My Documents/cpphome$ nm main.o | grep max | c++filt
0000000000000133 t _GLOBAL__sub_I__Z3maxIcET_S0_S0_
0000000000000000 T char max<char>(char, char)
0000000000000000 W int max<int>(int, int)
                 U mymax(int, int)
tony@Win10Bedroom:/mnt/c/Users/Tony Su/My Documents/cpphome$ nm other.o | grep max | c++filt
00000000000000d7 t _GLOBAL__sub_I__Z3maxIcET_S0_S0_
0000000000000000 T char max<char>(char, char)
0000000000000000 W int max<int>(int, int)
000000000000003e T mymax(int, int)

However, I start to be curious why nm gives different marks on Cygwin than on Ubuntu?? and Why linker on Cgywin can handle two T definitions correctly?

您需要了解 nm 输出 不会 为您提供全貌。

nm 是 binutils 的一部分,并使用 libbfd。它的工作方式是将各种目标文件格式解析为 libbfd 内部表示,然后 nm 等工具以 human-readable 格式打印该内部表示。

有些东西会“在翻译中丢失”。这就是你应该 例如objdump 查看 ELF 个文件(至少不查看 ELF 个文件的符号 table)。

正如您正确推断的那样,Linux 上允许使用多个 max<int>() 符号的原因是编译器将它们作为 W(弱定义)符号发出。

Windows也是如此,除了 Windows使用旧的COFF格式,没有弱符号。相反,符号被发送到一个特殊的 .linkonce.$name 部分,并且 linker 知道它可以 select 任何这样的部分进入 link,但应该只这样做 一次(即它知道丢弃任何其他目标文件中该部分的所有其他副本)。