【问题标题】:Why can't C++ deduce T in a call to Foo<T>::Foo(T&&)?为什么 C++ 不能在调用 Foo<T>::Foo(T&&) 时推导出 T?
【发布时间】:2020-02-29 23:38:08
【问题描述】:

给定以下模板结构:

template<typename T>
struct Foo {
    Foo(T&&) {}
};

这样编译,T 被推导出为int

auto f = Foo(2);

但这不能编译:https://godbolt.org/z/hAA9TE

int x = 2;
auto f = Foo(x);

/*
<source>:12:15: error: no viable constructor or deduction guide for deduction of template arguments of 'Foo'
    auto f = Foo(x);
             ^

<source>:7:5: note: candidate function [with T = int] not viable: no known conversion from 'int' to 'int &&' for 1st argument
    Foo(T&&) {}
    ^
*/

但是,Foo&lt;int&amp;&gt;(x) 被接受。

但是当我添加一个看似多余的用户定义的推导指南时,它起作用了:

template<typename T>
Foo(T&&) -> Foo<T>;

如果没有用户自定义的推演指南,为什么T不能推演为int&amp;

【问题讨论】:

标签: c++ templates language-lawyer


【解决方案1】:

这里的问题是,由于 是在 T 上模板化的,所以在构造函数 Foo(T&amp;&amp;) 中,我们执行类型推导;我们总是有一个 r 值参考。也就是说,Foo 的构造函数实际上是这样的:

Foo(int&&)

Foo(2) 有效,因为2 是prvalue。

Foo(x) 不会,因为x 是无法绑定到int&amp;&amp; 的左值。您可以通过 std::move(x) 将其转换为适当的类型 (demo)

Foo&lt;int&amp;&gt;(x) 工作得很好,因为由于引用折叠规则,构造函数变为Foo(int&amp;);最初它是Foo((int&amp;)&amp;&amp;),按照标准折叠为Foo(int&amp;)

关于您的“冗余”扣除指南: 最初有一个代码的默认模板推导指南,它基本上就像一个辅助函数,如下所示:

template<typename T>
struct Foo {
    Foo(T&&) {}
};

template<typename T>
Foo<T> MakeFoo(std::add_rvalue_reference_t<T> value)
{
   return Foo<T>(std::move(value));
}

//... 
auto f = MakeFoo(x);

这是因为标准规定这个(虚构的)模板方法具有与类相同的模板参数(只是T),后跟作为构造函数的任何模板参数(在这种情况下没有;构造函数没有模板化) .然后,函数参数的类型与构造函数中的相同。在我们的例子中,在实例化Foo&lt;int&gt; 之后,构造函数看起来像Foo(int&amp;&amp;),换句话说就是一个右值引用。因此上面使用了add_rvalue_reference_t

显然这是行不通的。

当您添加“冗余”扣除指南时:

template<typename T>
Foo(T&&) -> Foo<T>;

您允许编译器区分,尽管在构造函数(int&amp;const int&amp;int&amp;&amp; 等)中附加了任何类型的引用到 T,但您希望为类推断的类型没有参考(只是T)。这是因为我们突然正在执行类型推断。

现在我们生成另一个(虚构的)辅助函数,如下所示:

template<class U>
Foo<U> MakeFoo(U&& u)
{
   return Foo<U>(std::forward<U>(u));
}

// ...
auto f = MakeFoo(x);

(我们对构造函数的调用被重定向到辅助函数以进行类模板参数推导,因此Foo(x) 变为MakeFoo(x)

这允许U&amp;&amp; 变成int&amp;T 变成简单的int

【讨论】:

  • 第二层模板似乎没有必要;它提供什么价值?您能否提供一些文档的链接,以阐明 为什么 T&& 在此处始终被视为右值引用?
  • 但是如果还没有推导出T,那么“任何可转换为T的类型”是什么意思呢?
  • 您很快就提供了一种解决方法,但是您能否更专注于解释为什么它不起作用,除非您以某种方式对其进行修改?为什么它不能按原样工作? “x 是一个无法绑定到int&amp;&amp; 的左值”,但不理解的人会感到困惑,Foo&lt;int&amp;&gt;(x) 可以工作但不能自动找到 - 我想我们都想更深入地了解原因。
  • @Wyck:我已经更新了帖子以更多地关注原因。
  • @Wyck 真正的原因很简单:标准的专门关闭在合成演绎指南中完善转发语义,防止意外。
【解决方案2】:

我认为这里出现了混淆,因为综合演绎指南在转发引用方面存在一个特定的例外。

确实,构造函数生成的用于类模板参数推导的候选函数和用户定义的推导指南生成的候选函数看起来完全一样,即:

template<typename T>
auto f(T&&) -> Foo<T>;

但是对于从构造函数生成的,T&amp;&amp; 是一个简单的右值引用,而在用户定义的情况下它是一个转发引用。这是由 C++17 标准的[temp.deduct.call]/3 指定的(草案 N4659,强调我的):

转发引用是对 cv 非限定模板参数的右值引用不代表类模板的模板参数(在类模板参数推导期间([ over.match.class.deduct]))。

因此,从类构造函数合成的候选者不会像从转发引用一样推导出 T(这可以将 T 推导出为左值引用,因此 T&amp;&amp; 也是左值引用),而是只会将T 推断为非引用,因此T&amp;&amp; 始终是右值引用。

【讨论】:

  • 感谢您简洁明了的回答。你知道为什么在转发引用的规则中有类模板参数的例外吗?
  • @jtbandes 这似乎已因美国国家机构的评论而改变,请参阅论文p0512r0。虽然我找不到评论。我对基本原理的猜测是,如果您编写一个采用右值引用的构造函数,无论您指定Foo&lt;int&gt;(...) 还是仅指定Foo(...),您通常希望它以相同的方式工作,而转发引用的情况并非如此(这可能而是推导出Foo&lt;int&amp;&gt;
猜你喜欢
  • 1970-01-01
  • 2011-01-18
  • 1970-01-01
  • 2020-10-03
  • 1970-01-01
  • 2010-11-08
  • 1970-01-01
  • 1970-01-01
  • 2021-02-18
相关资源
最近更新 更多