SFINAE 可以进行演绎但无法进行替换

SFINAE works with deduction but fails with substitution

考虑以下 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

LIVE DEMO

问题:这是为什么? SFINAE 不应该在 替换 期间工作吗?然而在这里它似乎只在推导期间有效。

我不是语言律师,但我不认为在 class 中定义 using type = typename T::type; 本身可用作 SFINAE 以 enable/disable 接收一个函数class.

的对象

如果你想要一个解决方案,你可以将SFINAE应用到Wrapper版本如下

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

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

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

-- 编辑 --

OP精问

My Wrapper has many more dependencies on T, it would be impractical to duplicate them all in a SFINAE expression. Isn't there a way to check if Wrapper itself can be instantiated?

正如 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 类型(如果需要)。

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

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

自我介绍

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

第一次通话

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.F.

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

编译器够无辜吗?他(她?)达标了吗? @YSC has pointed out that [temp.over]/1 说:

When a call to the name of a function or function template is written (explicitly, or implicitly using the operator notation), template argument deduction ([temp.deduct]) and checking of any explicit template arguments ([temp.arg]) are performed for each function template to find the template argument values (if any) that can be used with that function template to instantiate a function template specialization that can be invoked with the call arguments. For each function template, if the argument deduction and checking succeeds, the template-arguments (deduced and/or explicit) are used to synthesize the declaration of a single function template specialization which is added to the candidate functions set to be used in overload resolution. If, for a given function template, argument deduction fails or the synthesized function template specialization would be ill-formed, no such function is added to the set of candidate functions for that template. The complete set of candidate functions includes all the synthesized declarations and all of the non-template overloaded functions of the same name. The synthesized declarations are treated like any other functions in the remainder of overload resolution, except as explicitly noted in [over.match.best].

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

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

  2. 换人。由于第一阶段已经失败,在这种情况下甚至没有达到这个阶段。 这个阶段出现的问题以SFINAE为准。

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

函数调用表达式中调用函数的推导分两步进行:

  1. 可行函数集的确定;
  2. 确定最佳可行函数。

活函数集只能包含函数声明模板函数特化声明.

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

  1. Substitution of explicitly provided template arguments (in names<A>(x,y) A is explicitly provided);(substitution是指在函数模板声明中,模板参数被它们的实参替换)
  2. 扣除未提供的模板参数;
  3. 替换推导的模板参数。

调用表达式test(a,b)

  1. 没有明确提供的模板参数。
  2. 第一个模板函数
  3. T被推导为A,第二个模板函数[temp.deduct.type]/8推导失败。所以第二个模板函数不会参与重载决议
  4. 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:

Third, for F to be a viable function, there shall exist for each argument an implicit conversion sequence that converts that argument to the corresponding parameter of F.

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