当我试图在我的库中为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版本
-
uncomemnt /删除return_type :: test的typedef
这个例子当然是简约和纯粹的学术 . 在我的库中,我遇到了这个问题,因为const和非const版本返回不同的类型 . 我通过使用部分专用的辅助类来解决这个问题 .
但为什么上面的例子导致了这种奇怪的行为呢?为什么编译器不想使用const版本有效且非匹配的非const版本?
1 回答
原因是执行函数调用解析的方式,以及模板参数推导和替换 .
首先,执行 name lookup . 这为您提供了两个具有匹配名称
foo()
的函数 .其次,执行 type deduction :对于每个具有匹配名称的模板函数,编译器会尝试推导出能够产生匹配的函数模板参数 . 你得到的错误发生在这个阶段 .
第三, overload resolution 进入游戏 . 这只是在执行了类型推导并且已经确定了用于解析调用的可行函数的签名之后,这是有道理的:只有在找到所有候选的确切签名之后,编译器才能有意义地解析函数调用 .
您得到与非const重载相关的错误的事实不是因为编译器选择它作为解析调用的最可行的候选者(这将是步骤3),而是因为编译器在实例化其返回类型时产生错误在步骤2中确定其签名 .
这是 not entirely obvious 为什么这会导致错误,因为有人可能会认为 SFINAE 适用(替换失败不是错误) . 为了澄清这一点,我们可以考虑一个更简单的例子:
在此示例中,SFINAE适用:在步骤2中,编译器将针对上述两个重载中的每一个推导出
T
,并尝试确定它们的签名 . 如果超载1,则会导致替换失败:X<const int>
未定义type
(X
中没有typedef
) . 但是,由于SFINAE,编译器只是 discards it 并发现重载2是可行的匹配 . 因此,它选择它 .现在让我们以一种反映你的例子的方式稍微改变一下这个例子:
更改的是,重载1不再返回
X<T>::type
,而是R<T>::type
. 由于R
中的typedef
声明,因此 same 依次为X<T>::type
,因此人们可能会期望它产生相同的结果 . 但是,在这种情况下,您会收到编译错误 . 为什么?标准有答案(第14.8.3 / 8段):
显然,第二个示例(以及您的)在 nested context 中生成错误,因此SFINAE不适用 . 我相信这回答了你的问题 .
顺便说一句,值得注意的是,这更常见(第14.8.2 / 2段):
如果您对事情发生变化的原因感到好奇, this paper 可能会给你一个想法 .