208

如果您阅读类似的代码

auto&& var = foo();

foo任何按类型值返回的函数在哪里T。然后var是一个右值引用类型的左值T。但这意味着var什么?这是否意味着,我们可以窃取资源var?是否有任何合理的情况,您应该使用auto&&类似的方式告诉读者您的代码,就像您在返回 aunique_ptr<>以告知您拥有独占所有权时所做的那样?例如什么T&&时候T是类类型?

auto&&我只是想了解,除了模板编程之外,是否还有其他用例;就像Scott Meyers 的文章Universal References中的示例中讨论的那样。

4

4 回答 4

294

By using auto&& var = <initializer> you are saying: I will accept any initializer regardless of whether it is an lvalue or rvalue expression and I will preserve its constness. This is typically used for forwarding (usually with T&&). The reason this works is because a "universal reference", auto&& or T&&, will bind to anything.

You might say, well why not just use a const auto& because that will also bind to anything? The problem with using a const reference is that it's const! You won't be able to later bind it to any non-const references or invoke any member functions that are not marked const.

As an example, imagine that you want to get a std::vector, take an iterator to its first element and modify the value pointed to by that iterator in some way:

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

This code will compile just fine regardless of the initializer expression. The alternatives to auto&& fail in the following ways:

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

So for this, auto&& works perfectly! An example of using auto&& like this is in a range-based for loop. See my other question for more details.

If you then use std::forward on your auto&& reference to preserve the fact that it was originally either an lvalue or an rvalue, your code says: Now that I've got your object from either an lvalue or rvalue expression, I want to preserve whichever valueness it originally had so I can use it most efficiently - this might invalidate it. As in:

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));

This allows use_it_elsewhere to rip its guts out for the sake of performance (avoiding copies) when the original initializer was a modifiable rvalue.

What does this mean as to whether we can or when we can steal resources from var? Well since the auto&& will bind to anything, we cannot possibly try to rip out vars guts ourselves - it may very well be an lvalue or even const. We can however std::forward it to other functions that may totally ravage its insides. As soon as we do this, we should consider var to be in an invalid state.

Now let's apply this to the case of auto&& var = foo();, as given in your question, where foo returns a T by value. In this case we know for sure that the type of var will be deduced as T&&. Since we know for certain that it's an rvalue, we don't need std::forward's permission to steal its resources. In this specific case, knowing that foo returns by value, the reader should just read it as: I'm taking an rvalue reference to the temporary returned from foo, so I can happily move from it.


As an addendum, I think it's worth mentioning when an expression like some_expression_that_may_be_rvalue_or_lvalue might turn up, other than a "well your code might change" situation. So here's a contrived example:

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;
}

Here, get_vector<T>() is that lovely expression that could be either an lvalue or rvalue depending on the generic type T. We essentially change the return type of get_vector through the template parameter of foo.

When we call foo<std::vector<int>>, get_vector will return global_vec by value, which gives an rvalue expression. Alternatively, when we call foo<std::vector<int>&>, get_vector will return global_vec by reference, resulting in an lvalue expression.

If we do:

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

We get the following output, as expected:

2
1
2
2

If you were to change the auto&& in the code to any of auto, auto&, const auto&, or const auto&& then we won't get the result we want.


An alternative way to change program logic based on whether your auto&& reference is initialised with an lvalue or rvalue expression is to use type traits:

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
}
于 2012-11-05T23:37:02.893 回答
14

首先,我建议阅读我的这个答案作为边读,以逐步解释通用引用的模板参数推导如何工作。

这是否意味着,我们可以窃取资源var

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

认为与inauto&&完全相同,因为它(几乎†</sup>)正是如此。当您需要传递它们或以任何方式使用它们时,您如何处理函数中的通用引用?您用于获取原始值类别。如果在传递给你的函数之前它是一个左值,那么它在传递之后保持一个左值如果它是一个右值,它将再次成为一个右值(请记住,命名的右值引用是一个左值)。T&&template<class T> void f(T&& v);std::forward<T>(v)std::forward

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

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

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


auto到目前为止的不同之处在于,这auto v = {1,2,3};将是v一个std::initializer_list,而f({1,2,3})将是一个扣除失败。

于 2012-11-05T16:05:10.300 回答
4

考虑一些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&&内部使用的代码应该传达它将在适当的情况下在内部使用移动语义。

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

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

于 2012-11-05T11:03:53.977 回答
-3

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

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

  2. &&是新的移动语义。支持移动语义的类型实现了一个构造函数,该构造函数T(T && 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 &&返回值 fromfoo并且item是对 中每个值的引用foo

于 2012-11-05T11:28:32.280 回答