SFINAE可以扣除,但不能替代

时间:2019-04-30 12:27:00

标签: c++ c++11 templates language-lawyer sfinae

考虑以下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是否应该在替代期间工作?但是,这里似乎只在推演期间起作用。

3 个答案:

答案 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。

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

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

  

编写对函数或函数模板名称的调用时   (显式或隐式使用运算符)   参数推导([temp.deduct])并检查任何显式   为每个函数执行模板参数([temp.arg])   模板以查找可以作为模板参数的值(如果有)   与该功能模板一起使用以实例化功能模板   可以使用call参数调用的特殊化。 对于每个   函数模板,如果参数推导和检查成功,   模板参数(推导和/或显式)用于   综合单个功能模板的声明   被添加到设置为   用于过载解析。如果对于给定的功能模板,   参数推导失败或综合功能模板   专业化将是不正确的,没有这样的功能被添加到   该模板的候选功能集。   候选函数包括所有综合声明和所有   相同名称的非模板重载函数。的   综合声明与   剩余的重载解决方案,除非另有明确说明   [over.match.best]。

缺少A::type会导致严重错误。阅读https://stackoverflow.com/a/15261234。基本上,确定type是否是所需的重载时有两个阶段:

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

  2. 替换。由于第一阶段已经失败,因此在这种情况下甚至无法达到该阶段。 此阶段出现的问题受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类型都启用了该功能,但是当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; }

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

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

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

答案 2 :(得分:1)

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

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

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

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

  1. 显式提供的模板参数的替换(在names<A>(x,y) A中被显式提供);(替代表示在函数模板代用中,模板参数被其参数替换)
  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

  

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

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