T &&(双&符号)在C++ 11中意味着什么?

pax*_*blo 747 c++ c++-faq rvalue-reference perfect-forwarding c++11

我一直在研究C++ 11的一些新功能,我注意到的是在声明变量时使用的双符号,例如T&& var.

首先,这只野兽叫什么?我希望谷歌允许我们搜索这样的标点符号.

究竟是什么意思?

乍一看,它似乎是一个双重参考(如C风格的双指针T** var),但我很难想到一个用例.

Pet*_*ene 637

它声明了一个右值引用(标准提案文档).

这是对右值参考的介绍.

以下是Microsoft标准库开发人员对rvalue引用的深入了解.(但在阅读本文之前,请参阅本答案后面的注释中的注意事项.)

C++ 03引用(现在称为C++ 11中的左值引用)之间的最大区别在于它可以像临时一样绑定到rvalue而不必是const.因此,此语法现在是合法的:

T&& r = T();
Run Code Online (Sandbox Code Playgroud)

右值引用主要提供以下内容:

移动语义.现在可以定义移动构造函数和移动赋值运算符,它采用右值引用而不是通常的const-lvalue引用.移动的功能类似于副本,除非它没有义务保持源不变; 实际上,它通常会修改源,使其不再拥有移动的资源.这对于消除无关副本非常有用,尤其是在标准库实现中.

例如,复制构造函数可能如下所示:

foo(foo const& other)
{
    this->length = other.length;
    this->ptr = new int[other.length];
    copy(other.ptr, other.ptr + other.length, this->ptr);
}
Run Code Online (Sandbox Code Playgroud)

如果这个构造函数是临时传递的,那么复制将是不必要的,因为我们知道临时将被销毁; 为什么不利用临时已分配的资源?在C++ 03中,没有办法阻止副本,因为我们无法确定我们是否临时传递.在C++ 11中,我们可以重载一个移动构造函数:

foo(foo&& other)
{
   this->length = other.length;
   this->ptr = other.ptr;
   other.length = 0;
   other.ptr = nullptr;
}
Run Code Online (Sandbox Code Playgroud)

注意这里的重大区别:移动构造函数实际上修改了它的参数.这将有效地将临时"移动"到正在构造的对象中,从而消除不必要的副本.

移动构造函数将用于临时和非const左值引用,这些引用使用std::move函数显式转换为rvalue引用(它只执行转换).以下代码都为f1和调用move构造函数f2:

foo f1((foo())); // Move a temporary into f1; temporary becomes "empty"
foo f2 = std::move(f1); // Move f1 into f2; f1 is now "empty"
Run Code Online (Sandbox Code Playgroud)

完美转发.右值引用允许我们正确转发模板化函数的参数.以此工厂功能为例:

template <typename T, typename A1>
std::unique_ptr<T> factory(A1& a1)
{
    return std::unique_ptr<T>(new T(a1));
}
Run Code Online (Sandbox Code Playgroud)

如果我们调用factory<foo>(5),那么参数将被推断为int&,即使foo构造函数需要,也不会绑定到文字5 int.好吧,我们可以使用A1 const&,但是如果foo通过非const引用获取构造函数参数呢?要制作一个真正通用的工厂功能,我们必须打开A1&和重载工厂A1 const&.如果工厂采用1参数类型,那可能没问题,但是每个额外的参数类型会将必要的重载数乘以2.这很快就无法维护.

rvalue引用通过允许标准库定义std::forward可以正确转发lvalue/rvalue引用的函数来解决此问题.有关std::forward工作原理的更多信息,请参阅此优秀答案.

这使我们能够像这样定义工厂函数:

template <typename T, typename A1>
std::unique_ptr<T> factory(A1&& a1)
{
    return std::unique_ptr<T>(new T(std::forward<A1>(a1)));
}
Run Code Online (Sandbox Code Playgroud)

现在,当传递给T构造函数时,参数的rvalue/lvalue-ness被保留.这意味着如果使用rvalue调用factory,则使用rvalue T调用构造函数.如果使用左值调用工厂,则使用左值T调用构造函数.由于一个特殊规则,改进的工厂功能起作用:

当函数参数类型的形式为T&&,其中T是一个模板参数,并且函数的参数的类型的一个左值A,类型A&用于模板参数推导.

因此,我们可以像这样使用工厂:

auto p1 = factory<foo>(foo()); // calls foo(foo&&)
auto p2 = factory<foo>(*p1);   // calls foo(foo const&)
Run Code Online (Sandbox Code Playgroud)

重要的右值参考属性:

  • 对于重载解析,左值更喜欢绑定到左值引用,而右值更喜欢绑定到右值引用.因此,临时为什么更喜欢在复制构造函数/赋值运算符上调用移动构造函数/移动赋值运算符.
  • rvalue引用将隐式绑定到rvalues和临时转换的临时值.ie float f = 0f; int&& i = f;格式正确,因为float可以隐式转换为int; 引用将是转换结果的临时值.
  • 命名的右值引用是左值.未命名的右值引用是右值. 这对于理解为什么std::move需要在以下方面进行调用非常重要:foo&& r = foo(); foo f = std::move(r);

  • `命名的右值引用的+1是左值.未命名的右值参考值是右值.不知道这一点,我一直在努力理解为什么人们会做一个"T && t; std :: move(t);`在移动ctors等很长一段时间. (59认同)
  • **注意**:MSDN上的链接文章("Rvalue参考:VC10中的C++ 0x特性,第2部分")_是对Rvalue引用的一个非常清晰的介绍,**但**使得有关Rvalue引用的声明为_once_在C++ 11标准草案中是真的,但对于最后一个,**不是真的**!具体来说,它在各个点上说rvalue引用可以绑定到左值,这曾经是真的,但[已更改](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2009/ n2844.html #intro).(例如`int x; int && rrx = x;`[不再编译](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=40486)在GCC中) (31认同)
  • 为了更好地理解,有人可以解释以下是否不正确的陈述 1. `rvalues` 可以被认为是生命周期无法保证的临时变量。2. `foo&amp;&amp; r = foo()` 在作用域的持续时间内延长了 `foo()` 返回的生命周期。3. `foo&amp;&amp; r` 和 `const foo&amp; r` 等价吗? (2认同)

Pup*_*ppy 80

它表示右值参考.除非另外明确生成,否则Rvalue引用仅绑定到临时对象.它们用于在某些情况下使对象更有效,并提供称为完美转发的工具,这极大地简化了模板代码.

在C++ 03中,您无法区分不可变左值的副本和右值.

std::string s;
std::string another(s);           // calls std::string(const std::string&);
std::string more(std::string(s)); // calls std::string(const std::string&);
Run Code Online (Sandbox Code Playgroud)

在C++ 0x中,情况并非如此.

std::string s;
std::string another(s);           // calls std::string(const std::string&);
std::string more(std::string(s)); // calls std::string(std::string&&);
Run Code Online (Sandbox Code Playgroud)

考虑这些构造函数背后的实现.在第一种情况下,字符串必须执行复制以保留值语义,这涉及新的堆分配.但是,在第二种情况下,我们事先知道传递给构造函数的对象是立即销毁的,并且它不必保持不变.在这种情况下,我们可以有效地交换内部指针而不执行任何复制,这实际上更有效.移动语义有益于任何具有昂贵或禁止复制内部引用资源的类.考虑一下std::unique_ptr- 现在我们的类可以区分临时和非临时,我们可以使移动语义正常工作,这样unique_ptr就不能复制但可以移动,这意味着std::unique_ptr可以合法地存储在标准容器中,排序,等等,而C++ 03 std::auto_ptr不能.

现在我们考虑rvalue引用的另一种用法 - 完美转发.考虑将引用绑定到引用的问题.

std::string s;
std::string& ref = s;
(std::string&)& anotherref = ref; // usually expressed via template
Run Code Online (Sandbox Code Playgroud)

不记得C++ 03对此有何看法,但在C++ 0x中,处理rvalue引用时的结果类型是至关重要的.对类型T的右值引用(其中T是引用类型)成为类型T的引用.

(std::string&)&& ref // ref is std::string&
(const std::string&)&& ref // ref is const std::string&
(std::string&&)&& ref // ref is std::string&&
(const std::string&&)&& ref // ref is const std::string&&
Run Code Online (Sandbox Code Playgroud)

考虑最简单的模板函数 - min和max.在C++ 03中,您必须手动重载const和非const的所有四种组合.在C++ 0x中,它只是一个重载.结合可变参数模板,可实现完美转发.

template<typename A, typename B> auto min(A&& aref, B&& bref) {
    // for example, if you pass a const std::string& as first argument,
    // then A becomes const std::string& and by extension, aref becomes
    // const std::string&, completely maintaining it's type information.
    if (std::forward<A>(aref) < std::forward<B>(bref))
        return std::forward<A>(aref);
    else
        return std::forward<B>(bref);
}
Run Code Online (Sandbox Code Playgroud)

我离开的返回类型推演,因为我不记得它是如何做副手,但分可以接受左值,右值,常量左值的任意组合.

  • 为什么你使用`std::forward&lt;A&gt;(aref) &lt; std::forward&lt;B&gt;(bref)`?当您尝试前进“int&amp;”和“float&amp;”时,我认为这个定义不正确。最好放弃一种类型的表单模板。 (2认同)

mmo*_*cny 25

T&& 用于类型推导(例如用于完美转发)的术语通俗地称为转发参考."通用参考"一词是由Scott Meyers 在本文中创造的,但后来被改变了.

那是因为它可能是r值或l值.

例如:

// template
template<class T> foo(T&& t) { ... }

// auto
auto&& t = ...;

// typedef
typedef ... T;
T&& t = ...;

// decltype
decltype(...)&& t = ...;
Run Code Online (Sandbox Code Playgroud)

可以在以下答案中找到更多讨论:通用引用的语法


kur*_*erg 11

右值引用是一种类似于普通引用X&的类型,但有一些例外.最重要的一点是,当涉及到函数重载解析时,左值更喜欢旧式左值引用,而右值更喜欢新的右值引用:

void foo(X& x);  // lvalue reference overload
void foo(X&& x); // rvalue reference overload

X x;
X foobar();

foo(x);        // argument is lvalue: calls foo(X&)
foo(foobar()); // argument is rvalue: calls foo(X&&)
Run Code Online (Sandbox Code Playgroud)

那么什么是右值?任何不是左值的东西.左值是一个表达式,它引用一个内存位置,并允许我们通过&运算符获取该内存位置的地址.

首先通过一个例子更容易理解rvalues的成就:

 class Sample {
  int *ptr; // large block of memory
  int size;
 public:
  Sample(int sz=0) : ptr{sz != 0 ? new int[sz] : nullptr}, size{sz} 
  {}
  // copy constructor that takes lvalue 
  Sample(const Sample& s) : ptr{s.size != 0 ? new int[s.size] :\
      nullptr}, size{s.size}
  {
     std::cout << "copy constructor called on lvalue\n";
  }

  // move constructor that take rvalue
  Sample(Sample&& s) 
  {  // steal s's resources
     ptr = s.ptr;
     size = s.size;        
     s.ptr = nullptr; // destructive write
     s.size = 0;
     cout << "Move constructor called on rvalue." << std::endl;
  }    
  // normal copy assignment operator taking lvalue
  Sample& operator=(const Sample& s)
  {
   if(this != &s) {
      delete [] ptr; // free current pointer
      ptr = new int[s.size]; 
      size = s.size; 
    }
    cout << "Copy Assignment called on lvalue." << std::endl;
    return *this;
  }    
 // overloaded move assignment operator taking rvalue
 Sample& operator=(Sample&& lhs)
 {
   if(this != &s) {
      delete [] ptr; //don't let ptr be orphaned 
      ptr = lhs.ptr;   //but now "steal" lhs, don't clone it.
      size = lhs.size; 
      lhs.ptr = nullptr; // lhs's new "stolen" state
      lhs.size = 0;
   }
   cout << "Move Assignment called on rvalue" << std::endl;
   return *this;
 }
//...snip
};     
Run Code Online (Sandbox Code Playgroud)

构造函数和赋值运算符已经过载了带有右值引用的版本.Rvalue引用允许函数在编译时(通过重载解析)在"我是在左值还是右值上调用?"条件下进行分支.这使我们能够在上面创建更有效的构造函数和赋值运算符,从而移动资源而不是复制它们.

编译器在编译时自动分支(取决于是否为左值或右值调用它),选择是否应调用移动构造函数或移动赋值运算符.

总结:rvalue引用允许移动语义(和完美的转发,在下面的文章链接中讨论).

一个易于理解的实用示例是类模板std :: unique_ptr.由于unique_ptr维护其底层原始指针的独占所有权,因此无法复制unique_ptr.这将违反他们对独家所有权的不变性.所以他们没有复制构造函数.但他们确实有移动构造函数:

template<class T> class unique_ptr {
  //...snip
 unique_ptr(unique_ptr&& __u) noexcept; // move constructor
};

 std::unique_ptr<int[] pt1{new int[10]};  
 std::unique_ptr<int[]> ptr2{ptr1};// compile error: no copy ctor.  

 // So we must first cast ptr1 to an rvalue 
 std::unique_ptr<int[]> ptr2{std::move(ptr1)};  

std::unique_ptr<int[]> TakeOwnershipAndAlter(std::unique_ptr<int[]> param,\
 int size)      
{
  for (auto i = 0; i < size; ++i) {
     param[i] += 10;
  }
  return param; // implicitly calls unique_ptr(unique_ptr&&)
}

// Now use function     
unique_ptr<int[]> ptr{new int[10]};

// first cast ptr from lvalue to rvalue
unique_ptr<int[]> new_owner = TakeOwnershipAndAlter(\
           static_cast<unique_ptr<int[]>&&>(ptr), 10);

cout << "output:\n";

for(auto i = 0; i< 10; ++i) {
   cout << new_owner[i] << ", ";
}

output:
10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 
Run Code Online (Sandbox Code Playgroud)

static_cast<unique_ptr<int[]>&&>(ptr)通常使用std :: move完成

// first cast ptr from lvalue to rvalue
unique_ptr<int[]> new_owner = TakeOwnershipAndAlter(std::move(ptr),0);
Run Code Online (Sandbox Code Playgroud)

一篇优秀的文章解释了所有这些以及更多(如rvalues如何允许完美转发及其含义)以及许多优秀的例子,是Thomas Becker的C++ Rvalue References Explained.这篇文章很大程度上依赖于他的文章.

较短的介绍是Stroutrup等人的Rvalue References简介.人