SFINAE可以扣除,但不能替代

rus*_*tyx 21 c++ templates sfinae language-lawyer c++11

考虑以下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;
}
Run Code Online (Sandbox Code Playgroud)

这里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
Run Code Online (Sandbox Code Playgroud)

现场演示

问题:为什么?SFINAE在换人期间不应该工作吗?但是在这里它似乎仅在推论过程中起作用。

L. *_* F. 21

自我介绍

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

第一次电话

test(a, b);     // works
Run Code Online (Sandbox Code Playgroud)

在此调用中,参数类型为A。首先让我考虑一下第一个重载:

template <class T>
void test(T, T);
Run Code Online (Sandbox Code Playgroud)

简单。T = A。现在考虑第二个:

template <class T>
void test(Wrapper<T>, Wrapper<T>);
Run Code Online (Sandbox Code Playgroud)

嗯...什么?Wrapper<T>A?我必须为世界上Wrapper<T>每种可能的类型实例化,T以确保不能使用类型Wrapper<T>为null的参数初始化可能是专用的type的参数A?好吧...我认为我不会那样做...

因此,我将不会实例化任何对象Wrapper<T>。我将选择第一个过载。

第二次通话

test<A>(a, b);  // doesn't work
Run Code Online (Sandbox Code Playgroud)

test<A>?啊哈,我不必演绎。让我检查两个重载。

template <class T>
void test(T, T);
Run Code Online (Sandbox Code Playgroud)

T = A。现在替换-签名为(A, A)。完善。

template <class T>
void test(Wrapper<T>, Wrapper<T>);
Run Code Online (Sandbox Code Playgroud)

T = A。现在取代...等等,我从未实例化Wrapper<A>?那我不能替代。我怎么知道这对于通话是否可行?好吧,我必须首先实例化它。(正在实例化)等等...

using type = typename T::type;
Run Code Online (Sandbox Code Playgroud)

A::type?错误!

回到LF

大家好,我是LF让我们回顾一下编译器的工作。

编译器足够纯真的吗?他(她?)符合标准吗? @YSC指出[temp.over] / 1表示:

编写对函数或函数模板名称的调用时(显式或隐式使用运算符),模板参数推导([temp.deduct])和任何显式模板参数([temp.arg])的检查都会被执行。对每个功能模板执行此操作,以找到可与该功能模板一起使用的模板参数值(如果有),以实例化可与调用参数一起调用的功能模板特化。对于每个函数模板,如果参数推导和检查成功,模板参数(推导和/或显式)用于综合单个函数模板特化的声明,该声明被添加到要在重载解析中使用的候选函数集中。如果对于给定的功能模板,自 变量推导失败,或者合成的功能模板专业化形式不正确,则不会将此类功能添加到该模板的候选功能集中。完整的候选函数集包括所有合成的声明以及同名的所有非模板重载函数。除非在[over.match.best]中有明确说明,否则在剩余的重载解析中,将综合声明与其他任何函数一样对待。

丢失type会导致硬错误。阅读/sf/answers/1068286411/。基本上,确定template<class T> void test(Wrapper<T>, Wrapper<T>)所需的过载是否有两个阶段:

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

  2. 替代。由于第一阶段已经失败,因此在这种情况下甚至无法达到该阶段。在此阶段发生的问题受SFINAE的约束。

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

  • @YSC必须实例化重载才能检查其有效性。我将更新我的答案以澄清这一点。 (3认同)
  • @StoryTeller好吧……给定类型为A的参数,就不能推论包装器&lt;T&gt;。甚至没有与“ A”类型的参数远程相关的“ Wrapper &lt;T&gt;”类型。由于甚至没有推论,可以实例化什么? (2认同)
  • 谢谢。“ *我必须为每种可能的类型T实例化Wrapper &lt;T&gt;。我将不这样做... *”的标准文章参考是什么? (2认同)

Oli*_*liv 1

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

  1. 确定可行功能集;
  2. 确定最佳可行功能。

可行函数集只能包含函数声明模板函数特化声明

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

  1. 显式提供的模板参数的替换(innames<A>(x,y) A是显式提供的);(替换是指在函数模板声明中,模板参数被其参数替换)
  2. 扣除未提供的模板实参;
  3. 替换推导的模板参数。

调用表达式test(a,b)

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

因此,该集合中只有一个重载,并且它是由重载决策选择的。

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

(在@TC和@geza的相关评论后编辑)

  1. 提供了模板参数:A并在两个模板函数的声明中替换它。这种替换仅涉及函数模板特化声明的实例化。所以这两个模板就可以了
  2. 不扣除模板参数
  3. 不替换推导的模板参数。

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

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

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