当我试图在我的库中为const和非const模板参数提供函数时,我遇到了一个奇怪的问题。以下源代码是一个最小的示例现象:
#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:错误:未定义模板的隐式实例化 'some_meta_class'
因此,由于某种原因,编译器希望将非const版本的foo用于const int-reference。这显然会导致上面的错误,因为some_meta_class没有实现。奇怪的是,如果您执行以下更改之一,代码编译良好并且有效:
这个例子当然是简约和纯粹的学术。在我的库中,我遇到了这个问题,因为const和非const版本返回不同的类型。我通过使用部分专用的辅助类来解决这个问题。
但为什么上面的例子导致了这种奇怪的行为呢?为什么编译器不想使用const版本有效且非常匹配的非const版本?
答案 0 :(得分:17)
原因是执行函数调用解析的方式,以及模板参数推导和替换。
首先,执行名称查找。这为您提供了两个具有匹配名称foo()
的函数。
其次,执行类型推导:对于具有匹配名称的模板函数的每个,编译器会尝试推导出会产生的函数模板参数一个可行的比赛。你得到的错误发生在这个阶段。
第三,重载决议进入游戏。这只是在执行了类型推导之后,并且已经确定了用于解析调用的可行函数的签名,这是有道理的:编译器只有在找到函数调用之后才能有意义地解析函数调用。所有候选人的确切签名。
您得到与非const重载相关的错误的事实并不是因为编译器选择它作为解析调用的最可行的候选者(这将是第3步),但是因为编译器在实例化时产生错误在步骤2中确定其签名的返回类型。
不完全明显但是为什么会导致错误,因为人们可能会认为 SFINAE 适用(替换失败不是错误)。为了澄清这一点,我们可以考虑一个更简单的例子:
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<const int>
未定义任何type
(typedef
中没有X
)。但是,由于SFINAE,编译器只是丢弃它并发现重载2是可行的匹配。因此,它选择它。
现在让我们稍微改变一下示例,以反映您的示例:
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<T>::type
,而是返回R<T>::type
。由于X<T>::type
中的typedef
声明,因此与R
相同,因此人们可能会期望它产生相同的结果。但是,在这种情况下,您会收到编译错误。为什么呢?
标准有答案(第14.8.3 / 8段):
如果替换导致无效的类型或表达式,则类型推导失败。如果使用替换参数写入,则无效的类型或表达式将是格式错误的。 [...]只有函数类型的直接上下文中的无效类型和表达式及其模板参数类型才会导致演绎失败。
显然,第二个示例(以及您的)在嵌套上下文中生成错误,因此SFINAE不适用。我相信这可以回答你的问题。
顺便说一下,有趣的是,这个自C ++ 03以来已经发生了变化,这更普遍地说明了(第14.8.2 / 2段):
[...]如果模板参数或函数模板的函数类型中的替换导致类型无效,则类型推导失败。 [...]
如果您对为什么事情发生变化的原因感到好奇, this paper 可能会给您一个想法。