auto&& 告诉我们什么?

What does auto&& tell us?

本文关键字:什么 我们 auto      更新时间:2023-10-16

如果您读取

的代码

auto&& var = foo();

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

我只想了解auto&&的其他用例,而不是模板编程中的用例;就像Scott Meyers的示例中所讨论的示例中所讨论的那样。

通过使用auto&& var = <initializer>,您是在说:我将接受任何初始化器无论它是LVALUE还是RVALUE表达式,我都将保留其constness 。这通常用于转发(通常使用T&&)。此原因是因为"通用引用" auto&&T&&将绑定到 nything

您可能会说,那么为什么不使用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,以保留它最初是LVALUE或RVALUE的事实,您的代码说:现在,我已经从lvalue或rvalue表达式中获得了您的对象,我想保留它最初拥有的任何珍贵性,以便我可以最有效地使用它 - 这可能使它无效。如:

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为了性能(避免副本)撕裂其肠道,当原始初始化器是可修改的rvalue时。

这意味着我们是否可以或何时可以从var窃取资源?好吧,由于auto&&将绑定到任何东西,因此我们不可能尝试撕开var S胆量 - 它很可能是一个lvalue甚至const。但是,我们可以 std::forward它可以完全破坏其内部的其他功能。一旦我们这样做,我们应该考虑var处于无效状态。

现在,让我们将其应用于您的问题中给出的auto&& var = foo();的情况,其中FOO返回T逐值。在这种情况下,我们确定var的类型将被推论为T&&。由于我们可以肯定地知道这是一个rvalue,因此我们不需要std::forward的许可来窃取其资源。在这种特定情况下,知道foo按值返回,读者应该将其读为:我正在对foo返回的临时级进行rvalue引用,因此我可以很高兴地从它。


作为附录,我认为值得一提的是,除了"好吧,您的代码可能会改变"情况外,诸如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而成为lvalue或rvalue。我们本质上通过foo的模板参数更改get_vector的返回类型。

当我们调用foo<std::vector<int>>时,get_vector将按值返回global_vec,该值提供了RVALUE表达式。另外,当我们致电foo<std::vector<int>&>时,get_vector将通过引用返回global_vec,导致LVALUE表达式。

如果我们这样做:

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&&是否使用lvalue或rvalue表达式来更改程序逻辑的替代方法是使用类型特征:

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
}

首先,我建议阅读我的答案,作为逐步解释的侧阅读,以解释模板参数如何扣除通用参考的扣除。

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

不一定。如果foo()突然返回了参考,或者您更改了呼叫,但忘了更新var的使用?或者,如果您使用通用代码,并且foo()的返回类型可能会根据您的参数而更改?

认为auto&&template<class T> void f(T&& v);中的T&&完全相同,因为它(几乎)完全相同。当您需要以任何方式将它们传递或使用它们时,您如何处理函数中的通用引用?您使用std::forward<T>(v)将原始值类别返回。如果在传递到您的功能之前是LVALUE,则在通过std::forward之后会保持LVALUE。如果是一个rvalue,它将再次成为一个rvalue(请记住,命名的rvalue参考是一个lvalue)。

那么,您如何以通用方式正确使用var?使用std::forward<decltype(var)>(var)。这将与上面函数模板中的std::forward<T>(v)完全相同。如果varT&&,您将获得一个rvalue,如果是T&,则您将获得一个lvalue。

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

个人,当我需要一个修改变量时,我在通用代码中使用auto&&。完美的rvalue正在修改,因为该动作操作可能会窃取其胆量。如果我只想变得懒惰(即,即使我知道即使不拼写类型名称),并且不需要修改(例如,仅在范围的打印元素时),我会坚持使用auto const&


auto在远方不同,以至于auto v = {1,2,3};将使v成为std::initializer_list,而f({1,2,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 &&)

移动构造函数仍然可用。


并解决您的其他问题:

...当您使用自动&amp;&amp;告诉读者您的代码...

正如Xeo所说,

第一件事本质上是我尽可能高效地传递X,无论X类型是什么。因此,在内部看到使用auto&&的代码,应该在适当的情况下传达其内部使用移动语义的代码。

...就像您返回suolty_ptr&lt;>时所做的那样,告诉您拥有独家所有权...

当功能模板采用类型T&&的参数时,它可能会移动您传递的对象。返回unique_ptr明确向呼叫者提供所有权;接受 T&& may 从呼叫者中删除所有权(如果存在ctor等等,等等)。

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

  1. auto零件使编译器可以根据上下文推导类型(在这种情况下为返回值)。这是没有任何参考资格的(允许您指定是否要TT &T &&进行推论类型T)。

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

这使您可以拥有类似的东西:

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

so:

auto var = foo();

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

auto &&var = foo();

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

这是在新的循环语法中使用的:

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

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

相关文章: