将模板成员函数的专门化定义(带OUT默认体)放在源文件中是否安全

Is it safe to place definition of specialization of template member function (withOUT default body) in source file?

本文关键字:默认 源文件 安全 是否 OUT 成员 函数 定义 专门化      更新时间:2023-10-16

我的意思是:

// 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错误,这对我来说是新的。所以,我"解决"了这个错误,现在一切似乎都很好,但仍然。。

这是定义明确的行为吗?


编辑:非成员模板函数也是如此(前向声明的非成员模板功能)。

Lightness Races in Orbit引用了其不符合标准零件的原因。附近可能还有其他人。

我将尝试用更简单的术语解释标准措辞的含义,希望我能正确理解,并最终解释链接器错误(或没有错误):

  1. 实例化有什么意义
  2. 编译器如何选择专业化
  3. 实例化时需要什么
  4. 为什么链接器出错

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遵循安腾ABI,我们可以简单地查找它是如何破坏符号的。事实证明,ABI在篡改专业化和隐式实例化方面没有什么区别,因此

cls.f( asd );

调用_ZN3cls1fIPKcEEvT_(其解映射为void cls::f<char const*>(char const*))和专用化:

template<>
void cls::f( const char* )
{
}

也产生CCD_ 6。

注意:我不清楚一个明确的专业化是否会被赋予不同的含义

不,我认为这不好:

[C++11: 14/6]:函数模板、类模板的成员函数或类模板的静态数据成员应在其隐式实例化的每个翻译单元中定义(14.7.1),除非在某些翻译单元中显式实例化了相应的专门化(14.7.2);不需要进行诊断。

[C++11: 14.7.3/6]:如果模板、成员模板或类模板的成员是显式专门化的,则应在首次使用该专门化之前声明该专门化,这将导致在发生这种使用的每个翻译单元中发生隐式实例化;不需要进行诊断[..]

坦率地说,我无法解释为什么它对你有效。

我认为您的原始代码不正确,您的"变通方法"也不符合标准(尽管编译器和链接器处理它)。@Lightness Races in Orbit的回答中引用了标准中的好语录。另请参阅标准中的以下示例([温度解释规范]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

我把我的答案标记为社区维基,因为事实上这只是一个大评论。