【问题标题】:What does auto&& tell us?auto&& 告诉我们什么?
【发布时间】:2012-10-25 04:37:36
【问题描述】:

如果你读过类似的代码

auto&& var = foo();

其中foo 是按T 类型的值返回的任何函数。那么var 是对T 的右值引用类型的左值。但这对var 意味着什么?这是否意味着,我们可以窃取var的资源?是否有任何合理的情况应该使用auto&& 告诉读者您的代码,就像您返回unique_ptr<> 告诉您拥有独占所有权时所做的一样?如果T 属于类类型,例如T&& 呢?

我只是想了解一下,auto&&除了模板编程还有其他用例吗?就像 Scott Meyers 在这篇文章 Universal References 中的示例中讨论的那样。

【问题讨论】:

  • 我一直在想同样的事情。我了解类型推导的工作原理,但是当我使用 auto&& 时,我的代码 是什么?我一直在考虑为什么基于范围的 for 循环扩展为使用 auto&& 作为示例,但还没有解决。或许谁回答都能解释清楚。
  • 这合法吗?我的意思是在foo 返回后立即销毁 T 的实例,将右值引用存储到它听起来像 UB 到 ne。
  • @aleguna 这是完全合法的。我不想返回指向局部变量的引用或指针,而是返回值。例如,函数foo 可能看起来像:int foo(){return 1;}
  • @aleguna 对临时对象的引用执行生命周期延长,就像在 C++98 中一样。
  • @aleguna 生命周期延长仅适用于本地临时对象,不适用于返回引用的函数。见*.com/a/2784304/567292

标签: c++ c++11 auto forwarding-reference


【解决方案1】:

通过使用auto&& var = <initializer>,您的意思是:我将接受任何初始化器,无论它是左值还是右值表达式,并且我将保留它的常量。这通常用于转发(通常使用T&&)。之所以可行,是因为“通用引用”auto&&T&& 将绑定到任何东西

你可能会说,为什么不直接使用const auto&,因为它会绑定到任何东西?使用const 引用的问题在于它是const!您以后将无法将其绑定到任何非 const 引用或调用任何未标记为 const 的成员函数。

举个例子,假设你想得到一个std::vector,将一个迭代器指向它的第一个元素,并以某种方式修改该迭代器指向的值:

auto&& vec = some_expression_that_may_be_rvalue_or_lvalue;
auto i = std::begin(vec);
(*i)++;

无论初始化表达式如何,这段代码都能正常编译。 auto&& 的替代方案在以下方面失败:

auto         => will copy the vector, but we wanted a reference
auto&        => will only bind to modifiable lvalues
const auto&  => will bind to anything but make it const, giving us const_iterator
const auto&& => will bind only to rvalues

因此,auto&& 完美运行!像这样使用auto&& 的示例是在基于范围的for 循环中。详情请见my other question

如果您随后在您的 auto&& 引用上使用 std::forward 以保留它最初是左值或右值的事实,您的代码将显示:现在我已经从左值中获取了您的对象或右值表达式,我想保留它最初具有的任何价值,以便我可以最有效地使用它 - 这可能会使它无效。 如:

auto&& var = some_expression_that_may_be_rvalue_or_lvalue;
// var was initialized with either an lvalue or rvalue, but var itself
// is an lvalue because named rvalues are lvalues
use_it_elsewhere(std::forward<decltype(var)>(var));

当原始初始化程序是可修改的右值时,这允许use_it_elsewhere 为了性能(避免复制)而撕掉它的胆量。

这对于我们是否可以或何时可以从var 窃取资源意味着什么?好吧,既然auto&amp;&amp; 将绑定到任何东西,我们不可能自己尝试撕掉vars 的胆量——它很可能是一个左值甚至是常量。但是,我们可以将其std::forward 用于可能完全破坏其内部的其他功能。一旦我们这样做,我们应该认为var 处于无效状态。

现在让我们将其应用于auto&amp;&amp; var = foo(); 的情况,如您的问题中给出的那样,其中 foo 按值返回T。在这种情况下,我们确定var 的类型将被推断为T&amp;&amp;。因为我们确定它是一个右值,所以我们不需要std::forward 的许可来窃取它的资源。在这种特定情况下,知道foo 按值返回,读者应该将其读作:我正在对从foo 返回的临时值进行右值引用,所以我可以愉快地离开它。


作为附录,我认为除了“你的代码可能会改变”的情况之外,何时可能出现像 some_expression_that_may_be_rvalue_or_lvalue 这样的表达式是值得一提的。所以这是一个人为的例子:

std::vector<int> global_vec{1, 2, 3, 4};

template <typename T>
T get_vector()
{
  return global_vec;
}

template <typename T>
void foo()
{
  auto&& vec = get_vector<T>();
  auto i = std::begin(vec);
  (*i)++;
  std::cout << vec[0] << std::endl;
}

这里,get_vector&lt;T&gt;() 是一个可爱的表达式,它可以是左值或右值,具体取决于泛型类型T。我们本质上是通过foo的模板参数来改变get_vector的返回类型。

当我们调用foo&lt;std::vector&lt;int&gt;&gt; 时,get_vector 将按值返回global_vec,它给出了一个右值表达式。或者,当我们调用foo&lt;std::vector&lt;int&gt;&amp;&gt; 时,get_vector 将通过引用返回global_vec,从而产生一个左值表达式。

如果我们这样做:

foo<std::vector<int>>();
std::cout << global_vec[0] << std::endl;
foo<std::vector<int>&>();
std::cout << global_vec[0] << std::endl;

正如预期的那样,我们得到以下输出:

2
1
2
2

如果您要将代码中的auto&amp;&amp; 更改为autoauto&amp;const auto&amp;const auto&amp;&amp; 中的任何一个,那么我们将不会得到我们想要的结果。


根据您的 auto&amp;&amp; 引用是使用左值还是右值表达式初始化来更改程序逻辑的另一种方法是使用类型特征:

if (std::is_lvalue_reference<decltype(var)>::value) {
  // var was initialised with an lvalue expression
} else if (std::is_rvalue_reference<decltype(var)>::value) {
  // var was initialised with an rvalue expression
}

【讨论】:

  • 我们不能简单地说 T vec = get_vector&lt;T&gt;(); inside function foo 吗?还是我将其简化到荒谬的程度:)
  • @Asterisk No bcoz T vec 只能在 std::vector 的情况下分配给左值,如果 T 是 std::vector 那么我们将使用按值调用效率低下
  • auto& 给了我同样的结果。我正在使用 MSVC 2015。GCC 会产生错误。
  • 这里我使用的是 MSVC 2015,auto& 给出的结果与 auto&& 相同。
  • @SeventhSon84 右值引用不能绑定到左值。因此,auto&amp;&amp; 在分配左值时被推断为int&amp;(左值引用)。但是int&amp;&amp; 是一个无条件的右值引用。所以你必须写int&amp;&amp; j = std::move(i);来表达你的意图,同时保证i以后不会被使用。
【解决方案2】:

首先,我建议阅读this answer of mine 作为旁白,以逐步解释通用引用的模板参数推导如何工作。

意思是说,我们可以盗取var的资源?

不一定。如果foo() 突然返回了一个引用,或者您更改了调用但忘记更新var 的使用怎么办?或者,如果您使用的是通用代码并且 foo() 的返回类型可能会根据您的参数而改变?

认为auto&amp;&amp;template&lt;class T&gt; void f(T&amp;&amp; v); 中的T&amp;&amp; 完全相同,因为它(几乎)完全一样。当您需要传递它们或以任何方式使用它们时,您如何处理函数中的通用引用?您使用std::forward&lt;T&gt;(v) 来获取原始值类别。如果它在传递给您的函数之前是一个左值,那么它在通过std::forward 之后仍然是一个左值。如果它是一个右值,它将再次成为一个右值(请记住,命名的右值引用是一个左值)。

那么,您如何以通用方式正确使用var?使用std::forward&lt;decltype(var)&gt;(var)。这将与上面函数模板中的std::forward&lt;T&gt;(v) 完全相同。如果varT&amp;&amp;,你会得到一个右值,如果是T&amp;,你会得到一个左值。

那么,回到主题:代码库中的auto&amp;&amp; v = f();std::forward&lt;decltype(v)&gt;(v) 告诉我们什么? 他们告诉我们,v 将以最有效的方式被获取和传递。 但是请记住,在转发了这样一个变量之后,它可能会被移出,所以它会在不重置的情况下继续使用它是不正确的。

就我个人而言,当我需要一个可修改变量时,我会在通用代码中使用auto&amp;&amp;。完美转发右值正在修改,因为移动操作可能会偷走它的胆量。如果我只是想偷懒(即,即使我知道也不拼写类型名称)并且不需要修改(例如,仅打印范围的元素时),我会坚持使用auto const&amp;


autoauto v = {1,2,3}; 的区别很大,v 将变为 std::initializer_list,而 f({1,2,3}) 将是推论失败。

【讨论】:

  • 在你回答的第一部分:我的意思是如果foo()返回一个值类型T,那么var(这个表达式)将是一个左值,它的类型(这个表达式)将是对T(即T&amp;&amp;)的右值引用。
  • @MWid:有道理,删除了第一部分。
【解决方案3】:

考虑一些类型T,它有一个移动构造函数,并假设

T t( foo() );

使用移动构造函数。

现在,让我们使用中间引用来捕获来自foo 的返回:

auto const &ref = foo();

这排除了移动构造函数的使用,因此返回值必须被复制而不是移动(即使我们在这里使用std::move,我们实际上也不能通过 const ref 移动)

T t(std::move(ref));   // invokes T::T(T const&)

但是,如果我们使用

auto &&rvref = foo();
// ...
T t(std::move(rvref)); // invokes T::T(T &&)

移动构造函数仍然可用。


并解决您的其他问题:

... 有什么合理的情况应该使用 auto&& 告诉读者您的代码...

正如 Xeo 所说,第一件事本质上是我尽可能高效地传递 X,无论 X 是什么类型。因此,看到在内部使用 auto&amp;&amp; 的代码应该表明它将在适当的情况下在内部使用移动语义。

...就像您返回 unique_ptr 以告知您拥有独占所有权时所做的那样...

当函数模板接受T&amp;&amp; 类型的参数时,它表示它可能会移动您传入的对象。返回unique_ptr 显式地将所有权授予调用者;接受T&amp;&amp; 可能删除调用者的所有权(如果存在移动ctor 等)。

【讨论】:

  • 我不确定您的第二个示例是否有效。你不需要完美的转发来调用移动构造函数吗?
  • 这是错误的。在这两种情况下都会调用复制构造函数,因为refrvref 都是左值。如果你想要移动构造函数,那么你必须写T t(std::move(rvref))
  • 您的第一个示例中的意思是 const ref:auto const &amp;
  • @aleguna - 你和 MWid 是对的,谢谢。我已经确定了答案。
  • @Useless 你是对的。但这并不能回答我的问题。你什么时候使用auto&amp;&amp;,你用auto&amp;&amp;告诉读者你的代码是什么?
【解决方案4】:

auto &amp;&amp; 语法使用了 C++11 的两个新特性:

  1. auto 部分允许编译器根据上下文(本例中的返回值)推断类型。这没有任何引用限定(允许您指定是否需要 TT &amp;T &amp;&amp; 用于推导类型 T)。

  2. &amp;&amp; 是新的移动语义。支持移动语义的类型实现了一个构造函数T(T &amp;&amp; other),它以最佳方式移动新类型中的内容。这允许对象交换内部表示,而不是执行深层复制。

这让你有类似的东西:

std::vector<std::string> foo();

所以:

auto var = foo();

将执行返回向量的副本(昂贵),但是:

auto &&var = foo();

将交换向量的内部表示(来自foo 的向量和来自var 的空向量),因此会更快。

这用于新的 for 循环语法:

for (auto &item : foo())
    std::cout << item << std::endl;

for 循环将auto &amp;&amp; 保存到foo 的返回值,item 是对foo 中每个值的引用。

【讨论】:

  • 这是不正确的。 auto&amp;&amp; 不会移动任何东西,它只会做一个参考。它是左值还是右值引用取决于用于初始化它的表达式。
  • 在这两种情况下都会调用移动构造函数,因为std::vectorstd::string 是可移动构造的。这与var的类型无关。
  • @MWid:实际上,对复制/移动构造函数的调用也可以用 RVO 完全省略。
  • @MatthieuM。你说的对。但我认为在上面的例子中,copy cnstructor 永远不会被调用,因为一切都是 moveconstructible。
  • @MWid:我的意思是即使移动构造函数也可以省略。省略胜过移动(它更便宜)。