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

这完全没问题,对吧?

我开始怀疑这一点,因为我刚刚遇到了这个specialization of '...' after instantiation错误,这对我来说是新的。所以,我“解决”了这个错误,现在一切似乎都很好,但仍然......

这是定义明确的行为吗?


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

4

3 回答 3

6

轨道中的 Lightness Races引用了为什么它不符合标准的部分。附近可能还有其他人。

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

  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 遵循Itanium ABI,我们可以简单地查看它是如何破坏符号的。事实证明,ABI 在处理特化和隐式实例化方面没有区别,因此

cls.f( asd );

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

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

还产生_ZN3cls1fIPKcEEvT_

注意:我不清楚是否可以给明确的专业化赋予不同的修饰。

于 2014-05-02T14:12:43.790 回答
5

不,我不认为这没关系:

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

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

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

于 2014-04-10T13:43:22.783 回答
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,因为实际上它只是一个大评论。

于 2014-04-10T15:00:47.987 回答