考虑以下MCVE
struct A {};
template<class T>
void test(T, T) {
}
template<class T>
class Wrapper {
using type = typename T::type;
};
template<class T>
void test(Wrapper<T>, Wrapper<T>) {
}
int main() {
A a, b;
test(a, b); // works
test<A>(a, b); // doesn't work
return 0;
}
这里test(a, b);
起作用,而test<A>(a, b);
失败并显示以下信息:
<source>:11:30: error: no type named 'type' in 'A'
using type = typename T::type;
~~~~~~~~~~~~^~~~
<source>:23:13: note: in instantiation of template class 'Wrap<A>' requested here
test<A>(a, b); // doesn't work
^
<source>:23:5: note: while substituting deduced template arguments into function template 'test' [with T = A]
test<A>(a, b); // doesn't work
问题:为什么? SFINAE是否应该在替代期间工作?但是,这里似乎只在推演期间起作用。
答案 0 :(得分:20)
大家好,我是一个纯真的编译器。
test(a, b); // works
在此调用中,参数类型为A
。让我首先考虑第一个重载:
template <class T>
void test(T, T);
容易。 T = A
。
现在考虑第二个:
template <class T>
void test(Wrapper<T>, Wrapper<T>);
嗯...什么? Wrapper<T>
的{{1}}?我必须为世界上每种可能的类型A
实例化Wrapper<T>
只是为了确保类型T
的参数(可能是专门的)不能用以下参数初始化:输入Wrapper<T>
吗?好吧...我不认为我会这么做...
因此,我将不会实例化任何A
。我将选择第一个过载。
Wrapper<T>
test<A>(a, b); // doesn't work
?啊哈,我不必演绎。让我检查两个重载。
test<A>
template <class T>
void test(T, T);
。现在替换—签名为T = A
。完美。
(A, A)
template <class T>
void test(Wrapper<T>, Wrapper<T>);
。现在,......等等,我从未实例化T = A
?那我不能替代。我怎么知道这对于通话是否可行?好吧,我必须首先实例化它。 (实例化)等待...
Wrapper<A>
using type = typename T::type;
?错误!
大家好,我是L.F。让我们回顾一下编译器的工作。
编译器足够纯真的吗?他(她?)符合标准吗? @YSC指出[temp.over]/1说:
编写对函数或函数模板名称的调用时 (显式或隐式使用运算符) 参数推导([temp.deduct])并检查任何显式 为每个函数执行模板参数([temp.arg]) 模板以查找可以作为模板参数的值(如果有) 与该功能模板一起使用以实例化功能模板 可以使用call参数调用的特殊化。 对于每个 函数模板,如果参数推导和检查成功, 模板参数(推导和/或显式)用于 综合单个功能模板的声明 被添加到设置为 用于过载解析。如果对于给定的功能模板, 参数推导失败或综合功能模板 专业化将是不正确的,没有这样的功能被添加到 该模板的候选功能集。 候选函数包括所有综合声明和所有 相同名称的非模板重载函数。的 综合声明与 剩余的重载解决方案,除非另有明确说明 [over.match.best]。
缺少A::type
会导致严重错误。阅读https://stackoverflow.com/a/15261234。基本上,确定type
是否是所需的重载时有两个阶段:
实例化。在这种情况下,我们(完全)实例化template<class T> void test(Wrapper<T>, Wrapper<T>)
。在此阶段,Wrapper<A>
存在问题,因为using type = typename T::type;
不存在。 此阶段出现的问题是硬错误。
替换。由于第一阶段已经失败,因此在这种情况下甚至无法达到该阶段。 此阶段出现的问题受SFINAE约束。
是的,无辜的编译器做对了。
答案 1 :(得分:4)
我不是语言律师,但我不认为在类中定义using type = typename T::type;
本身可用作SFINAE来启用/禁用接收该类对象的功能。
如果需要解决方案,可以按如下所示将SFINAE应用于Wrapper
版本
template<class T>
auto test(Wrapper<T>, Wrapper<T>)
-> decltype( T::type, void() )
{ }
通过这种方式,此test()
功能仅对其中定义了T
类型的type
类型启用。
在您的版本中,每种T
类型都启用了该功能,但是当T
与Wrapper
不兼容时会出错。
-编辑-
OP精确询问
我的包装器对T的依赖更多,要在SFINAE表达式中全部复制它们是不切实际的。没有办法检查包装器本身是否可以实例化?
如Holt所建议的,您可以创建一个自定义类型特征以查看类型是否为Wrapper<something>
类型。例如
template <typename>
struct is_wrapper : public std::false_type
{ };
template <typename T>
struct is_wrapper<Wrapper<T>> : public std::true_type
{ using type = T; };
然后,您可以修改Wrapper
版本以接收U
类型,并检查U
是否为Wrapper<something>
类型
template <typename U>
std::enable_if_t<is_wrapper<U>{}> test (U, U)
{ using T = typename is_wrapper<U>::type; }
请注意,您可以使用T
结构内的type
定义来恢复原始的is_wrapper
类型(如果需要)。
如果您需要Wrapper
的非test()
版本,则此解决方案必须在T
是Wrapper<something>
类型时明确禁用它,以免发生冲突>
template <typename T>
std::enable_if_t<!is_wrapper<T>{}> test(T, T)
{ }
答案 2 :(得分:1)
对函数调用表达式中调用的函数的推导分两个步骤进行:
可行函数集只能包含函数声明和模板函数专门化声明。
因此,当调用表达式(test(a,b)
或test<A>(a,b)
)命名模板函数时,有必要确定所有模板参数:这称为模板参数推导。这是通过三个步骤[temp.deduct]执行的:
names<A>(x,y)
A
中被显式提供);(替代表示在函数模板代用中,模板参数被其参数替换)test(a,b)
T
推导为A
,对于第二个模板函数[temp.deduct.type]/8,推导失败。因此第二个模板函数将不参与重载解析A
替换为第一个模板函数的声明。替换成功。因此,集合中只有一个重载,并且它是由重载分辨率选择的。
test<A>(a,b)
(在@ T.C。和@geza的相关说明后编辑)
A
,并在两个模板函数的声明中将其替换。该替换仅涉及功能模板专门化的声明的实例化。所以这两个模板很好因此,两个模板专长test<A>(A,A)
和test<A>(Wrapper<A>,Wrapper<A>)
参与了重载解析。首先,编译器必须确定哪个函数是可行的。为此,编译器需要找到一个隐式转换序列,该序列将函数参数转换为函数参数类型[over.match.viable]/4:
第三,为了使F成为可行的函数,每个参数都应存在一个隐式转换序列,该转换序列将该参数转换为F的相应参数。
对于第二次重载,为了找到到Wrapper<A>
的转换,编译器需要此类的定义。因此,它(隐式)实例化了它。此实例导致观察到的编译器生成的错误。