SFINAE使用演绎,但用替换失败

SFINAE works with deduction but fails with substitution

本文关键字:替换 失败 演绎 SFINAE      更新时间:2023-10-16

请考虑以下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在替代期间不应该起作用吗?然而,在这里,它似乎只在演绎期间起作用。

自我介绍

大家好,我是一个无辜的编译器。

第一个电话

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>A?我必须为世界上每个可能的T类型实例化Wrapper<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;

A::type?错误!

回到L.

大家好,我是L.F。让我们回顾一下编译器做了什么。

编译器是否足够无辜?他(她?)是否符合标准?@YSC指出,[temp.over]/1 说:

写入对函数

或函数模板名称的调用时 (显式或隐式使用运算符表示法),模板 参数推导([temp.deduc])和检查任何明确的 为每个函数执行模板参数 ([temp.arg]) 模板以查找可以是 与该函数模板一起使用以实例化函数模板 可以使用调用参数调用的专用化。对于每个 函数模板,如果参数推导和检查成功, 模板参数(推导和/或显式)用于 合成单个函数模板的声明 添加到候选函数集的专业化 用于重载解析。如果,对于给定的函数模板,参数推导失败或合成函数模板失败 专业化格式不正确,没有这样的功能添加到 该模板的候选函数集。全套 候选函数包括所有合成声明和所有 同名的非模板重载函数。这 合成声明被视为与 过载解析的其余部分,除非在 [过度.匹配.最佳]。

缺少type会导致硬错误。阅读 https://stackoverflow.com/a/15261234。基本上,在确定template<class T> void test(Wrapper<T>, Wrapper<T>)是否是所需的重载时,我们有两个阶段:

  1. 实例。在这种情况下,我们(完全)实例化Wrapper<A>.在此阶段,using type = typename T::type;是有问题的,因为不存在A::type此阶段出现的问题是硬错误。

  2. 替代。由于第一阶段已经失败,因此在这种情况下甚至没有达到此阶段。此阶段发生的问题受SFINAE的影响。

所以是的,无辜的编译器做了正确的事情。

我不是语言律师,但我不认为在类中定义using type = typename T::type;本身可以用作 SFINAE 来启用/禁用接收该类对象的函数。

如果需要解决方案,可以将SFINAE应用于Wrapper版本,如下所示

template<class T>
auto test(Wrapper<T>, Wrapper<T>)
-> decltype( T::type, void() )
{ }

这样,此test()函数仅对内部定义了type类型的T启用。

在您的版本中,为每种T类型启用,但当TWrapper不兼容时出错。

--编辑--

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

请注意,您可以使用is_wrapper结构中的type定义恢复原始T类型(如果需要)。

如果您需要非Wrapper版本的test(),使用此解决方案,您必须在TWrapper<something>类型时明确禁用它以避免冲突

template <typename T>
std::enable_if_t<!is_wrapper<T>{}> test(T, T)
{ }

<</div> div class="answers">函数调用表达式中调用的函数的推导分两步执行:

  1. 确定一组可行的功能;
  2. 确定最佳可行功能。

可行函数集只能包含函数声明模板函数专用声明

所以当一个调用表达式(test(a,b)test<A>(a,b))命名一个模板函数时,需要确定所有的模板参数:这称为模板参数推导。分三步执行[温度扣除]:

  1. 替换显式提供的模板参数(names<A>(x,y)A显式提供);(替换意味着在函数模板设计中,模板参数被替换为它们的参数)
  2. 扣除未提供的模板参数;
  3. 替换推导的模板参数。

调用表达式test(a,b)

  1. 没有显式提供的模板参数。
  2. T推导第一个模板函数的A,则第二个模板函数 [temp.deduct.type]/8 的推导失败。所以第二个模板函数不会参与重载解析
  3. A在第一个模板函数的声明中被替换。替换成功。

因此,集合中只有一个重载,它是通过重载分辨率选择的。

调用表达式test<A>(a,b)

(在@T.C.和@geza的有关评论之后编辑)

  1. 提供了模板参数:A,并在两个模板函数的声明中替换它。此替换仅涉及函数模板专用化声明的实例化。所以两个模板很好
  2. 不扣除模板参数
  3. 没有替换推导的模板参数。

因此,两个模板专用化(test<A>(A,A)test<A>(Wrapper<A>,Wrapper<A>))参与重载解析。首先,编译器必须确定哪些函数是可行的。为此,编译器需要找到一个隐式转换序列,将函数参数转换为函数参数类型 [over.match.viable]/4:

第三,为了使 F 成为一个可行的函数,每个参数都应该存在一个隐式转换序列,该序列将该参数转换为 F 的相应参数。

对于第二个重载,为了找到Wrapper<A>的转换,编译器需要此类的定义。所以它(隐式)实例化它。正是这种实例化导致了编译器生成的观察到的错误。