将模板成员函数的专业化定义(没有默认主体)放在源文件中是不是安全?
Posted
技术标签:
【中文标题】将模板成员函数的专业化定义(没有默认主体)放在源文件中是不是安全?【英文标题】:Is it safe to place definition of specialization of template member function (withOUT default body) in source file?将模板成员函数的专业化定义(没有默认主体)放在源文件中是否安全? 【发布时间】:2014-05-24 05:35:41 【问题描述】:这就是我的意思:
// test.h
class cls
public:
template< typename T >
void f( T t );
;
-
// test.cpp
template<>
void cls::f( const char* )
-
// main.cpp
int main()
cls c;
double x = .0;
c.f( x ); // gives EXPECTED undefined reference (linker error)
const char* asd = "ads";
c.f( asd ); // works as expected, NO errors
return 0;
这完全没问题,对吧?
我开始怀疑这一点,因为我刚刚遇到了 specialization of '...' after instantiation
错误,这对我来说是新的。所以,我“解决”了这个错误,现在一切似乎都很好,但仍然......
这是明确定义的行为吗?
编辑: 对于非成员模板函数(前向声明的非成员模板函数)也是如此。
【问题讨论】:
啊,对不起。我误解了这个问题 见this question 并回答它。 你使用什么编译器?我记得 VC++ 违反了 [temp.expl.spec] 14.7.3/6 中提到的标准要求,并被 @Lightness Races in Orbit 引用。 @Constructor - gcc 4.4.5 @KirilKirov 尝试更新版本。最新版本的 gcc 更符合标准。 【参考方案1】:Lightness Races in Orbit 引用了为什么它不符合标准部分。附近可能还有其他人。
我将尝试用更简单的术语解释标准措辞的含义,希望我能正确理解,最后解释链接器错误(或不存在错误):
-
实例化的意义何在?
编译器如何选择特化?
在实例化时需要什么?
为什么会出现链接器错误?
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_
(解码为void cls::f<char const*>(char const*)
)和专业化:
template<>
void cls::f( const char* )
也会产生_ZN3cls1fIPKcEEvT_
。
注意:我不清楚一个明确的专业化是否可以被赋予不同的修饰。
【讨论】:
【参考方案2】:不,我觉得不行:
[C++11: 14/6]:
函数模板、类模板的成员函数或类模板的静态数据成员应在隐式实例化 (14.7.1) 的每个翻译单元中定义,除非相应的特化被显式实例化(14.7.2) 在某些翻译单元中;不需要诊断。
[C++11: 14.7.3/6]:
如果模板、成员模板或类模板的成员被显式特化,则应在第一次使用该特化之前声明该特化,这将导致在每个翻译单元中发生隐式实例化发生此类使用的地点;不需要诊断。 [..]
坦率地说,我无法解释为什么它对你有用。
【讨论】:
“坦率地说,我无法解释为什么它对你有用。” 嗯,在另一个 TU 中有一些带有外部链接的符号void cls::f<const char*>( const char* )
。所以可以找到符号。 IIRC,您可以使用已声明但未定义的函数模板(就像普通函数一样);您可以在同一个 TU 中调用后提供定义。
如果不是显式实例化,template<> void cls::f( const char* )
是什么?
不,等等。 14.7.2.清楚地定义了“显式实例化”。啊,我讨厌这种充满前向定义的描述风格:(
@Oktalist 如何满足第二个要求(关于第一次使用前的显式特化声明)? main.cpp
中的代码如何知道主模板的显式特化是在其他翻译单元中定义的?
@Constructor 哎呀,我想象的 OP 代码的一部分实际上并不存在(头文件中显式特化的 声明)。当然,这就是缺少的。【参考方案3】:
我认为您的原始代码不正确,并且您的“解决方法”也不符合标准(尽管您的编译器和链接器会处理它)。在@Lightness Races in Orbit 的answer 中引用了该标准的良好引用。另请参阅标准中的以下示例([temp.expl.spec] 14.7.3/6):
class String ;
template<class T> class Array /* ... */ ;
template<class T> void sort(Array<T>& v) /* ... */
void f(Array<String>& v)
sort(v); // use primary template
// sort(Array<T>&), T is String
template<> void sort<String>(Array<String>& v); // error: specialization
// after use of primary template
template<> void sort<>(Array<char*>& v); // OK: sort<char*> not yet used
我将我的答案标记为社区 wiki,因为实际上它只是一个大评论。
【讨论】:
以上是关于将模板成员函数的专业化定义(没有默认主体)放在源文件中是不是安全?的主要内容,如果未能解决你的问题,请参考以下文章