在源文件中存放模板成员函数的特化定义(不带默认体)是否安全?

9
这是我的意思:
// 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;
}

这完全没问题,对吧?

我开始怀疑了,因为我刚刚遇到了一个新的错误:实例化后对 '...' 的特化,这对我来说是新的。所以,我“解决”了这个错误,现在一切似乎都很正常,但是...

这是明确定义的行为吗?


编辑:对于非成员模板函数(前置声明的非成员模板函数)同样适用。


2
啊,对不起。我误解了问题。 - SirGuy
请查看这个问题以及它的答案。 - Constructor
你使用哪个编译器?据我记得,VC++ 违反了标准要求,在 [temp.expl.spec] 14.7.3/6 中有所注明,并被 @Lightness Races in Orbit 引用。 - Constructor
@构造函数 - gcc 4.4.5 - Kiril Kirov
@KirilKirov 尝试使用更新的版本。最新的gcc版本更符合标准。 - Constructor
@Constructor - 是的,我会尽力而为,因为在这里,我不能使用不同的版本。谢谢你提供的其他问题参考。 - Kiril Kirov
3个回答

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)"

这可以被延迟,因此可能与精确的调用站点不匹配,用于从其他模板调用的模板:

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);

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_

注意:我不确定是否可以为显式特化给出不同的名称重整。


5
不,我认为这并不好:
[C++11: 14/6]:函数模板、类模板的成员函数或类模板的静态数据成员应在每个隐式实例化它们的翻译单位中定义(14.7.1),除非对应的特化已在某个翻译单位中显式实例化(14.7.2);无需进行诊断。
[C++11: 14.7.3/6]:如果一个模板、一个类模板的成员模板或成员被显式地专门化,那么该专门化应在第一次使用该专门化以导致隐式实例化发生之前,在每个出现这样使用的翻译单位中声明;无需进行诊断。[..]
坦白地说,我无法解释为什么它对你有效。

“坦白说,我无法解释为什么它适用于您。” 嗯,在另一个TU中有一些标记void cls :: f <const char *>(const char *) 具有外部链接。 因此,可以找到符号。 如果我记得正确,您可以使用已声明但未定义的函数模板(就像普通函数一样); 在同一TU中在调用后提供定义。 - dyp
我认为是这样的:请参见14.7第4段。但措辞似乎有点令人困惑。 - R. Martinho Fernandes
1
不,等等。14.7.2清楚地定义了“显式实例化”。唉,我讨厌这种充满前向定义的描述风格:( - R. Martinho Fernandes
1
@Oktalist 它如何满足第二个要求(关于在第一次使用之前声明显式特化)?main.cpp中的代码如何知道主模板的显式特化是在其他翻译单元中定义的? - Constructor
2
@Constructor 哎呀,我想象了 OP 代码中实际不存在的部分(在头文件中显式特化的声明)。当然,这就是缺失的部分。 - Oktalist
显示剩余12条评论

2

我认为你的原始代码是不正确的,你的“解决方法”也不符合标准(尽管你的编译器和链接器可以处理它)。在这个回答中,@Lightness Races in Orbit引用了标准中的好例子。此外,还可以参考标准中的以下示例([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,因为实际上它只是一条大评论。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接