在源文件中放置模板成员函数(without default body)的特化定义是否安全?

Kir*_*rov 8 c++ templates member-functions template-specialization default-implementation

这就是我的意思:

// test.h
class cls
{
public:
    template< typename T >
    void f( T t );
};
Run Code Online (Sandbox Code Playgroud)

-

// test.cpp
template<>
void cls::f( const char* )
{
}
Run Code Online (Sandbox Code Playgroud)

-

// 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;
}
Run Code Online (Sandbox Code Playgroud)

这完全没问题吧?

我开始怀疑这一点,因为我只是碰到了specialization of '...' after instantiation错误,这对我来说是新的.所以,我"解决"这个错误,现在一切似乎工作正常,但仍然..

这是明确定义的行为吗?


编辑:对于非成员模板函数(前向声明的非成员模板函数)也是如此.

Mat*_* M. 6

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)"
Run Code Online (Sandbox Code Playgroud)

对于从其他模板调用的模板,它可能会延迟,因此与确切的调用站点不匹配:

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)"
Run Code Online (Sandbox Code Playgroud)

这种延迟非常重要,因为它可以写入:

  • 共同递归模板(即相互引用的模板)
  • 用户专业化

(*) 粗略地说,有一些例外,例如模板类的非模板方法......


2/ 编译器如何选择专业化?

在实例化时,编译器需要能够:

  • 决定调用哪个基本模板函数
  • 并且可能,调用它的哪个专业

这个旧的GotW展示了专业化的困境......但简而言之:

template <typename T> void foo(T);   // 1
template <typename T> void foo(T*);  // 2
Run Code Online (Sandbox Code Playgroud)

重载,每个都产生了一个不同的可能专业化家族,它们是它们的基础

template <> void foo<int>(int);
Run Code Online (Sandbox Code Playgroud)

是 1 的特化,并且

template <> void foo<int*>(int*);
Run Code Online (Sandbox Code Playgroud)

是2的专业。

为了解决函数调用,编译器会首先选择最佳的重载,同时忽略模板特化,然后,如果它选择了模板函数,则检查它是否有任何可以更好地应用的特化。


3/ 在实例化时需要什么?

因此,从编译器解析调用的方式,我们理解为什么标准规定任何特化都应该它的第一个实例化点之前声明。否则,它根本不会被考虑。

因此,在实例化的时候,人们需要已经看到:

  • 要使用的基本模板函数的声明
  • 要选择的专业的声明,如果有的话

但是定义呢?

不需要。编译器假定它将稍后在 TU 中提供,或者完全由另一个 TU 提供。

注意:它确实给编译器带来了负担,因为这意味着它需要记住它遇到的所有隐式实例化,并且它不能发出函数体,以便当它最终遇到定义时,它可以(最后)发出所有必要的代码对于它遇到的所有专业。我想知道为什么选择这种特殊方法,也想知道为什么即使没有extern声明 TU 也可能以未定义的函数体结尾。


4/ 为什么会出现链接器错误?

由于没有提供定义,gcc 相信您稍后提供它,并且只是发出对未解析符号的调用。如果您碰巧与提供此符号的另一个 TU 链接,则一切都会好起来,否则您将收到链接器错误。

由于 gcc 遵循Itanium ABI,我们可以简单地查看它如何处理符号。事实证明,ABI 在处理特化和隐式实例化方面没有区别,因此

cls.f( asd );
Run Code Online (Sandbox Code Playgroud)

调用_ZN3cls1fIPKcEEvT_(作为 demangles void cls::f<char const*>(char const*))和专业化:

template<>
void cls::f( const char* )
{
}
Run Code Online (Sandbox Code Playgroud)

也产生_ZN3cls1fIPKcEEvT_.

注意:我不清楚是否可以对显式专业化进行不同的修改。


Lig*_*ica 5

不,我认为这不行:

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

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

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

  • @Constructor 哎呀,我想象了一部分实际上并不存在的 OP 代码(头文件中显式专业化的 _declaration_)。当然,这就是所缺少的。 (2认同)