关于c ++:未定义模板’class’的隐式实例化

implicit instantiation of undefined template 'class'

在我的库中,当试图为常量和非常量模板参数提供函数时,我遇到了一个奇怪的问题。以下源代码是最小的示例现象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>


template<typename some_type>
struct some_meta_class;

template<>
struct some_meta_class<int>
{
    typedef void type;
};



template<typename some_type>
struct return_type
{
    typedef typename some_meta_class< some_type >::type test;

    typedef void type;
};



template<typename type>
typename return_type<type>::type foo( type & in )
{
    std::cout <<"non-const" << std::endl;
}

template<typename type>
void foo( type const & in )
{
    std::cout <<"const" << std::endl;
}


int main()
{
    int i;

    int const & ciref = i;
    foo(ciref);
}

我尝试为foo实现非const版本和const版本,但不幸的是,这段代码不能在clang 3.0和gcc 4.6.3上编译。

main.cpp:18:22: error: implicit instantiation of undefined template
'some_meta_class'

因此出于某种原因,编译器希望使用foo的非常量版本作为const int引用。这显然会导致上面的错误,因为没有为一些元类实现。奇怪的是,如果您进行以下更改之一,代码就会编译良好并工作正常:

  • 取消注释/删除非常量版本
  • 取消注释/删除返回类型":test"的typedef

这个例子当然是极简主义和纯粹的学术。在我的库中,我遇到了这个问题,因为const和non-const版本返回不同的类型。我通过使用一个部分专用的助手类来管理这个问题。

但为什么上面的例子会导致这种奇怪的行为呢?为什么编译器不想使用常量版本有效且匹配性更好的非常量版本?


原因是函数调用解析的执行方式,以及模板参数的推导和替换。

  • 首先,执行名称查找。这为您提供了两个具有匹配名称foo()的函数。

  • 其次,执行类型推导:对于每个具有匹配名称的模板函数,编译器尝试推导出能够产生可行匹配的函数模板参数。在这个阶段会发生错误。

  • 第三,过载分辨率进入游戏。只有在执行了类型推断并确定了用于解析调用的可行函数的签名之后,这才是合理的:编译器只有在找到所有候选函数的准确签名之后,才能有意义地解析函数调用。

  • 得到与非常量重载相关的错误的事实并不是因为编译器选择它作为解决调用的最可行的候选者(即步骤3),而是因为编译器在步骤2中实例化其返回类型以确定其签名时产生错误。

    但是,这并不完全清楚为什么会导致错误,因为人们可能会期望sfinae应用(替换失败不是错误)。为了澄清这一点,我们可以考虑一个简单的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    template<typename T> struct X { };

    template<typename T> typename X<T>::type f(T&) { }  // 1
    template<typename T> void f(T const&) { }           // 2

    int main()
    {
        int const i = 0;
        f(i); // Selects overload 2
    }

    在本例中,sfinae适用:在步骤2中,编译器将为上述两个重载中的每一个推导T,并尝试确定它们的签名。在过载1的情况下,这会导致替换失败:X没有定义任何type(X中没有typedef。但是,由于sfinae的原因,编译器只是丢弃它,发现重载2是一个可行的匹配。因此,它选择了它。

    现在,让我们稍微改变一下示例,以反映您的示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    template<typename T> struct X { };

    template<typename Y>
    struct R { typedef typename X<Y>::type type; };

    // Notice the small change from X<T> into R<T>!
    template<typename T> typename R<T>::type f(T&) { }  // 1
    template<typename T> void f(T const&) { }           // 2

    int main()
    {
        int const i = 0;
        f(i); // ERROR! Cannot instantiate R<int const>
    }

    改变的是,重载1不再返回X::type,而是返回R::type。这反过来与X::type相同,因为R中的typedef声明,所以人们可能期望它产生相同的结果。但是,在这种情况下,您会得到一个编译错误。为什么?

    本标准有答案(第14.8.3/8段):

    If a substitution results in an invalid type or expression, type deduction fails. An invalid type or expression is one that would be ill-formed if written using the substituted arguments. [...] Only invalid types and expressions in the immediate context of the function type and its template parameter types can result in a deduction failure.

    显然,第二个示例(以及您的示例)在嵌套上下文中生成错误,因此sfinae不适用。我相信这回答了你的问题。

    顺便说一下,有趣的是,自从C++ 03以来,这一点已经改变了,这一点更一般地说了(段落14.82/2):

    [...] If a substitution in a template parameter or in the function type of the function template results in an invalid type, type deduction fails. [...]

    如果你对事情发生变化的原因感到好奇,本文可能会给你一个想法。