什么是汽车&&告诉我们?

时间:2012-11-05 10:46:05

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

如果您阅读

之类的代码

auto&& var = foo();

其中foo是按类型T的值返回的任何函数。然后var是对T的rvalue类型引用的左值。但这对var意味着什么呢?这是否意味着,我们被允许窃取var的资源?是否有任何合理的情况,您应该使用auto&&告诉读者您的代码与您返回unique_ptr<>以告诉您拥有独占所有权时的情况类似?例如,当T&&属于类类型时,T呢?

我只是想了解,如果auto&&的任何其他用例比模板编程中的那些;与Scott Meyers撰写的本文Universal References中的示例中讨论的内容类似。

4 个答案:

答案 0 :(得分:185)

使用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

如果您在std::forward引用上使用auto&&来保留它最初是左值或右值的事实,那么您的代码会说:现在我已经得到了您的对象无论是左值还是右值表达式,我都希望保留它最初具有的价值,这样我才能最有效地使用它 - 这可能会使它无效。如:

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&&将绑定到任何东西,我们不可能试图撕掉var自己的胆量 - 它很可能是一个左值甚至是常量。但是,我们可以std::forward将其用于其他可能完全破坏其内部的功能。一旦我们这样做,我们应该考虑var处于无效状态。

现在让我们将此应用于auto&& var = foo();的情况,如您的问题所示,其中foo按值返回T。在这种情况下,我们确定var的类型将推断为T&&。由于我们确定它是一个右值,因此我们不需要std::forward的许可来窃取其资源。在这种特定情况下,知道foo按值返回,读者应该只读它:我正在对从{{1}返回的临时值进行右值引用所以我很乐意从中移开。


作为附录,我认为值得一提的是,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的模板参数更改了get_vector的返回类型。

当我们致电foo时,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&&autoauto&const auto&中的任何一个,那么我们将无法获得我们想要的结果。


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

auto&&

答案 1 :(得分:11)

首先,我建议阅读this answer of mine作为侧面阅读,逐步解释通用引用的模板参数推断是如何工作的。

  

是否意味着,我们被允许窃取var的资源?

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

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

那么,您如何以通用方式正确使用var?使用std::forward<decltype(var)>(var)。这与上面的函数模板中的std::forward<T>(v)完全相同。如果varT&&,您将获得左值,如果是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})将是扣减失败。

答案 2 :(得分:2)

考虑一些具有移动构造函数的类型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&amp;&amp; amp;和告诉读者你的代码......

正如Xeo所说,第一件事就是我尽可能高效地传递X ,无论X是什么类型。因此,看到内部使用auto&&的代码应该传达它将在适当的时候在内部使用移动语义。

  

...就像你返回unique_ptr&lt;&gt;时一样告诉你你拥有独家所有权...

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

答案 3 :(得分:-3)

auto &&语法使用C ++ 11的两个新功能:

  1. auto部分允许编译器根据上下文(在这种情况下为返回值)推断出类型。这没有任何参考资格(允许您为推断的类型T指定是否需要T &T &&T

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

  3. 这允许你有类似的东西:

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

    所以:

    auto var = foo();
    

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

    auto &&var = foo();
    

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

    这用于新的for循环语法:

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

    for-loop持有auto &&foo的返回值,而item是对foo中每个值的引用。