轨道中的 Lightness Races引用了为什么它不符合标准的部分。附近可能还有其他人。
我将尝试用更简单的术语解释标准措辞的含义,希望我能正确理解,最后解释链接器错误(或没有错误):
- 实例化有什么意义?
- 编译器如何选择专业化?
- 实例化时需要什么?
- 为什么会出现链接器错误?
1/实例化有什么意义?
模板函数的实例化点是调用或引用它的点 ( &std::sort<Iterator>
),所有模板参数都充实 (*)。
template <typename T>
void foo(T) { std::cout << typeid(T).name() << "\n"; }
int main() { foo(1); } // point of instantiation of "foo<int>(int)"
但是,对于从其他模板调用的模板,它可能会延迟,因此与确切的调用站点不匹配:
template <typename T>
void foo(T) { std::cout << typeid(T).name() << "\n"; }
template <typename T>
void bar(T t) { foo(t); } // not a point of instantiation, T is still "abstract"
int main() { foo(1); } // point of instantiation of "bar<int>(int)"
// and ALSO of "foo<int>(int)"
这种延迟非常重要,因为它可以写入:
(*) 粗略地说,有模板类的非模板方法等例外情况...
2/ 编译器如何选择专业化?
在实例化点,编译器需要能够:
- 决定调用哪个基本模板函数
- 并且可能,调用它的哪个专业
这个旧的GotW展示了专业化的困境......但简而言之:
template <typename T> void foo(T); // 1
template <typename T> void foo(T*); // 2
是重载,并且每个都产生一个不同的可能的特化系列,它们是它们的基础。
template <> void foo<int>(int);
是 1 的特化,并且
template <> void foo<int*>(int*);
是2的专精。
为了解决函数调用,编译器将首先选择最好的重载,同时忽略模板特化,然后,如果它选择了一个模板函数,检查它是否有任何可以更好地应用的特化。
3/ 在实例化时需要什么?
因此,从编译器解析调用的方式,我们可以理解为什么标准规定任何特化都应该在其第一个实例化点之前声明。否则,根本不会考虑。
因此,在实例化时,需要已经看到:
- 要使用的基本模板函数的声明
- 要选择的专业的声明(如果有)
但是定义呢?
它不是必需的。编译器假定它将稍后在 TU 中提供或完全由另一个 TU 提供。
注意:它确实给编译器增加了负担,因为这意味着它需要记住它遇到的所有隐式实例化,并且它无法发出函数体,因此当它最终遇到定义时,它可以(最终)发出所有必要的代码它遇到的所有专业。我想知道为什么选择了这种特殊的方法,也想知道为什么即使没有extern
声明,TU 也可能以未定义的函数体结束。
4/ 为什么是链接器错误?
由于没有提供定义,gcc 相信您稍后会提供它并简单地发出对未解析符号的调用。如果您碰巧与另一个提供此符号的 TU 链接,那么一切都会好起来的,否则您将收到链接器错误。
由于 gcc 遵循Itanium ABI,我们可以简单地查看它是如何破坏符号的。事实证明,ABI 在处理特化和隐式实例化方面没有区别,因此
cls.f( asd );
调用_ZN3cls1fIPKcEEvT_
(其 demangles as void cls::f<char const*>(char const*)
)和专业化:
template<>
void cls::f( const char* )
{
}
还产生_ZN3cls1fIPKcEEvT_
。
注意:我不清楚是否可以给明确的专业化赋予不同的修饰。