如果你读代码

Auto&& var = foo();

其中foo是任意返回值为t类型的函数,那么var是一个引用t的右值类型的左值,但这对var意味着什么?这是否意味着,我们可以窃取var的资源?是否存在一些合理的情况,您应该使用auto&&来告诉您代码的读者,就像您返回unique_ptr<>来告诉您具有独占所有权一样?举个例子,T&&是类类型的时候呢?

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


考虑具有move构造函数的某种类型T,并假设

T t( foo() );

使用move构造函数。

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

auto const &ref = foo();

这就排除了使用move构造函数,所以返回值必须被复制而不是移动(即使我们在这里使用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 &&)

move构造函数仍然可用。


为了回答你的其他问题:

... 是否存在一些合理的情况,你应该使用auto&&来告诉你的代码的读者一些事情…

首先,正如Xeo所说,我尽可能高效地传递X,不管X类型是什么。因此,看到在内部使用auto&&的代码时,应该告知它将在适当的情况下在内部使用move语义。

... 就像你返回一个unique_ptr<>来告诉你拥有独占所有权一样……

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


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

auto部分允许编译器根据上下文(本例中为返回值)推断类型。这没有任何引用限定(允许您指定是否需要T、T &或T &&用于推导的类型T)。 &&是新的move语义。支持移动语义的类型实现了构造函数T(T && other),该构造函数以最佳方式移动新类型中的内容。这允许对象交换内部表示,而不是执行深度复制。

这允许你有如下内容:

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

So:

auto var = foo();

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

auto &&var = foo();

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

在新的for循环语法中使用:

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

其中for循环包含foo返回值的auto &&, item是foo中每个值的引用。


首先,我建议你阅读我的回答,作为一个侧面阅读,一步一步地解释了通用引用的模板参数演绎是如何工作的。

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

不一定。如果foo()突然返回一个引用,或者您更改了调用,但忘记更新var的使用,该怎么办?或者如果你在泛型代码和foo()的返回类型可能会改变取决于你的参数?

Think of auto&& to be exactly the same as the T&& in template<class T> void f(T&& v);, because it's (nearly†) exactly that. What do you do with universal references in functions, when you need to pass them along or use them in any way? You use std::forward<T>(v) to get the original value category back. If it was an lvalue before being passed to your function, it stays an lvalue after being passed through std::forward. If it was an rvalue, it will become an rvalue again (remember, a named rvalue reference is an lvalue).

如何正确使用var呢?使用std::转发< decltype (var) > (var)。这将与上面函数模板中的std::forward<T>(v)完全相同。如果var是T&&,你会得到一个右值,如果它是T&,你会得到一个左值。

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

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


†auto与auto v ={1,2,3}相差甚远;将使v成为std::initializer_list,而f({1,2,3})将是一个演绎失败。


通过使用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循环中。详见我的另一个问题。

如果你在你的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_else为了性能(避免复制)而去掉它的部分。

这意味着我们是否可以或何时可以从var中窃取资源?因为auto&&会绑定到任何东西,我们不可能试图自己去掉vars的内脏——它很可能是一个左值,甚至是const。然而,我们可以std::将其转发给其他可能完全破坏其内部的函数。一旦我们这样做,我们应该认为var处于无效状态。

现在让我们把这个应用到auto&& var = foo();的情况,就像你的问题中给出的那样,其中foo按值返回一个T。在这种情况下,我们可以确定var的类型将被推导为T&&。因为我们确定它是一个右值,所以我们不需要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<T>()是一个可爱的表达式,它可以是一个左值,也可以是一个右值,这取决于泛型类型T。我们实际上通过foo的模板形参改变了get_vector的返回类型。

当调用foo<std::vector<int>>时,get_vector将按值返回global_vec,它给出一个右值表达式。或者,当调用foo<std::vector<int>&>时,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&&更改为auto、auto&、const auto&或const auto&&中的任意一个,那么我们将得不到我们想要的结果。


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

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
}