【问题标题】:SFINAE works with deduction but fails with substitutionSFINAE 适用于扣除但因替代而失败
【发布时间】:2019-09-19 02:48:44
【问题描述】:

考虑以下 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&lt;A&gt;(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 不应该工作吗?然而在这里它似乎只在演绎期间起作用。

【问题讨论】:

  • test(a, b);中,T不能推导出test的第二个重载,所以这个重载在Wrapper&lt;A&gt;的实例化之前被丢弃了,所以候选列表只包含首先是test,这就是它起作用的原因。在第二种情况下,第二个重载是有效的候选者,因为显式提供了 T,因此暗示 Wrapper&lt;A&gt; 的实例化失败。
  • 只是好奇:你在这里的意图是什么?根据::type成员类型的存在选择不同的重载?
  • @LF 的目的是将模板限制为 Wrapper&lt;T&gt; 类型,看起来它有时像 SFINAE 一样工作,但显然它没有。

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


【解决方案1】:

自我介绍

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

第一次通话

test(a, b);     // works

在这个调用中,参数类型是A。让我先考虑第一个重载:

template <class T>
void test(T, T);

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

template <class T>
void test(Wrapper<T>, Wrapper<T>);

嗯……什么? Wrapper&lt;T&gt;A?我必须为世界上所有可能的类型T 实例化Wrapper&lt;T&gt;,以确保不能使用A 类型的参数初始化可能是专用的Wrapper&lt;T&gt; 类型的参数?嗯......我不认为我会这样做......

因此我不会实例化任何Wrapper&lt;T&gt;。我会选择第一个重载。

第二次调用

test<A>(a, b);  // doesn't work

test&lt;A&gt;?啊哈,我不用扣分。让我检查一下这两个重载。

template <class T>
void test(T, T);

T = A。现在替换——签名是(A, A)。完美。

template <class T>
void test(Wrapper<T>, Wrapper<T>);

T = A。现在subst ...等等,我从来没有实例化Wrapper&lt;A&gt;?那我换不了。我怎么知道这是否会成为通话的可行过载?好吧,我必须先实例化它。 (实例化)等待......

using type = typename T::type;

A::type?错误!

回到 L.F.

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

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

当调用函数名或函数模板时 (显式或隐式使用运算符符号),模板 论据推导([temp.deduct])并检查任何显式 为每个函数执行模板参数 ([temp.arg]) 模板来查找模板参数值(如果有的话) 与该函数模板一起使用以实例化函数模板 可以使用调用参数调用的专业化。 对于每个 函数模板,如果参数推导和检查成功, 模板参数(推导和/或显式)用于 综合单个函数模板的声明 添加到候选函数集的特化 用于重载决议。如果,对于给定的函数模板, 推论失败或合成函数模板 专业化将是不正确的,没有这样的功能被添加到 该模板的候选函数集。 候选函数包括所有综合声明和所有 的同名非模板重载函数。这 综合声明被视为与任何其他函数一样 重载决议的其余部分,除非明确指出 [over.match.best]。

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

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

  2. 替换。由于第一阶段已经失败,在这种情况下甚至没有达到这个阶段。 此阶段出现的问题受 SFINAE 约束。

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

【讨论】:

  • @YSC 必须实例化重载才能检查其有效性。我将更新我的答案以澄清这一点。
  • @StoryTeller Well ... 给定A 类型的参数,Wrapper&lt;T&gt; 无法推断。没有类型Wrapper&lt;T&gt; 甚至与A 类型的参数远程相关。既然连推导都没有,那还能实例化什么呢?
  • 对不起,什么?我不记得依赖于论点的非推断上下文。它是参数的一个属性。
  • @StoryTeller "但包括 Wrapper" - 好吧,包括 Wrapper&lt;whatever&gt;whatever 可以推断出来。由于无法推断出whatever,因此从候选列表中丢弃此重载,并且不会实例化Wrapper&lt;whatever&gt;
  • 谢谢。 “我必须为每种可能的类型 T 实例化 Wrapper。我不打算这样做......”的标准文章参考是什么。
【解决方案2】:

我不是语言律师,但我认为在类中定义 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精确询问

我的 Wrapper 对 T 有更多的依赖,在 SFINAE 表达式中复制它们是不切实际的。有没有办法检查 Wrapper 本身是否可以实例化?

按照 Holt 的建议,您可以创建自定义类型特征来查看类型是否为 Wrapper&lt;something&gt; 类型;举例

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&lt;something&gt; 类型

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&lt;something&gt; 类型时明确禁用它以避免冲突

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

【讨论】:

  • 我的 WrapperT 有更多的依赖,在 SFINAE 表达式中复制它们是不切实际的。有没有办法检查Wrapper&lt;T&gt; 本身是否可以实例化?此外,真的很想知道为什么它不起作用。
  • @rustyx - 考虑一下,但我的第一个想法(在declval() 中使用std::declval&lt;Wrapper&lt;T&gt;&gt;() 而不是T::type)给出了与您的案例相同的硬错误......关于“为什么?”问题,对不起,我不是专家。
  • @rustyx 您需要在模板参数中隐藏Wrapper&lt;T&gt; 并使用特征对此模板参数执行测试,例如is_wrapper...并且您需要在@ 时禁用其他重载987654354@ 不是包装器。
  • @Holt - 您介意我添加您的解决方案示例吗?还是您更喜欢自己添加答案?
  • @max66 随意添加示例,这里是一个未完整测试的sn-p:godbolt.org/z/Los-fJ
【解决方案3】:

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

  1. 确定一组可行函数;
  2. 确定最佳可行函数。

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

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

  1. 显式提供的模板参数的替换(在names&lt;A&gt;(x,y) A 中显式提供);(替换意味着在函数模板声明中,模板参数被其参数替换)
  2. 未提供的模板参数的扣除;
  3. 替换推导的模板参数。

调用表达式test(a,b)

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

所以集合中只有一个重载,它是通过重载决议选择的。

调用表达式test&lt;A&gt;(a,b)

(根据@T.C.和@geza的相关言论编辑)

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

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

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

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

【讨论】:

  • 谢谢,有道理。您能否提供标准中的相关引用,说明第一种情况下为什么扣除失败test(a, b)
  • @rustyx 我在timsong-cpp.github.io/cppwp/n4659/temp.deduct.type#8 的答案中添加了链接。 P 和 A 必须具有相同的形式 template-name
  • 替换效果很好。一个函数在被重载决议选择之前不会被“调用”。但是,评估该函数是否是可行的候选函数需要检查 A 是否可以转换为 Wrapper&lt;A&gt;,这需要实例化 Wrapper&lt;A&gt;
  • 我认为您对答案的最后一部分有些错误。使用const Wrapper&lt;T&gt; &amp;,编译器不必检查Wrapper&lt;A&gt;是否是完全定义的对象(类似于Wrapper&lt;T&gt; &amp;),但编译失败。
  • @T.C.确实......我认为当我开始编辑答案时,这就是我设置重载解决方案的原因,然后我改变主意是错误的!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-12-23
  • 1970-01-01
  • 1970-01-01
  • 2013-06-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多