Lightness Races in Orbit引用了标准中的一些与其不兼容的部分。在附近可能还有其他类似情况。
我将尝试用简单易懂的语言解释标准术语的含义,希望能够正确理解,并最终解释链接错误(或无错误):
- 什么是实例化点?
- 编译器如何选择特化?
- 实例化点需要什么?
- 为什么会出现链接错误?
1/ 什么是实例化点?
模板函数的实例化点是调用或引用它时(&std::sort<Iterator>
)所有模板参数都被完全确定(*)的地方。
template <typename T>
void foo(T) { std::cout << typeid(T).name() << "\n"; }
int main() { foo(1); }
这可以被延迟,因此可能与精确的调用站点不匹配,用于从其他模板调用的模板:
template <typename T>
void foo(T) { std::cout << typeid(T).name() << "\n"; }
template <typename T>
void bar(T t) { foo(t); }
int main() { foo(1); }
这个延迟非常重要,因为它使得以下内容成为可能:
(*) 粗略地说,有一些例外情况,比如模板类的非模板方法...
2/ 编译器如何选择一个特化?
在实例化的时候,编译器需要能够:
- 决定调用哪个基本模板函数
- 可能还需要决定调用其中的哪个特化版本
这篇旧的GotW展示了特化的问题... 简而言之:
template <typename T> void foo(T)
template <typename T> void foo(T*)
这里涉及到的是重载,每个重载会产生一个不同的族群,其中它们是基础。
template <> void foo<int>(int);
IT技术中的一种专业化领域,属于1。
template <> void foo<int*>(int*);
是2的一个专业领域。
为了解析函数调用,编译器将首先选择最佳重载,同时忽略模板特化,然后,如果它选择了模板函数,则检查它是否有任何更好适用的特化。
3 / 实例化点需要什么?
因此,从编译器解决调用的方式,我们了解到标准规定任何特化都应在其第一个实例化点之前声明的原因。否则,它根本不会被考虑。
因此,在实例化点,需要已经看到:
- 要使用的基础模板函数的声明
- 要选择的特化的声明(如果有)
但是定义呢?
它不是必需的。编译器假定它将稍后在TU中或由另一个TU完全提供。
注意:这会给编译器带来负担,因为它意味着它需要记住所有遇到的隐式实例化,对于它不能发出函数体的实例化,当它最终遇到定义时,它可以(最后)发出所有必要的代码以处理它遇到的所有特化。我想知道为什么选择了这种特定的方法,也想知道为什么即使在没有extern
声明的情况下,TU也可能以未定义的函数体结束。
4 / 为什么是链接器错误?
由于没有提供定义,gcc信任您稍后提供它并简单地发出对未解析符号的调用。如果您恰好与提供此符号的另一个TU链接,则一切都将很好,否则您将获得链接器错误。
由于gcc遵循Itanium ABI,因此我们可以简单地查找它如何编码符号。事实证明,ABI在编码特化和隐式实例化时没有区别,因此
cls.f( asd );
调用了 _ZN3cls1fIPKcEEvT_
(它被解析为void cls::f<char const*>(char const*)
)和这个特化版本:
template<>
void cls::f( const char* )
{
}
同时还会产生_ZN3cls1fIPKcEEvT_
。
注意:我不确定是否可以为显式特化给出不同的名称重整。