T &&(双“&”号)在C ++ 11中是什么意思?


799

我一直在研究C ++ 11的一些新功能,而我注意到的一个是在声明变量(如)时使用了双“&”号T&& var

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

这到底是什么意思?

乍一看,它似乎是一个双重引用(如C样式的双指针T** var),但是我很难考虑到这种情况的用例。


55
我已将其添加到c ++常见问题解答中,因为我确信它会在将来出现更多。
GManNickG 2011年


41
您可以使用Google进行搜索,只需将短语用引号引起来即可:google.com/#q= "T%26%26“现在将您的问题作为第一匹配。:)
2011年

这里有一个很好的,易于理解的对类似问题的回答stackoverflow.com/questions/7153991/…–
丹尼尔(Daniel)

2
我在Google顶部搜索“ c ++两个&符”参数时遇到了三个stackoverflow问题,而你是第一个。因此,如果您可以拼出“两个&符参数”,则甚至不需要为此使用标点符号。
sergiol

Answers:


668

它声明一个右值参考(标准建议文档)。

这是右值引用的简介

这是Microsoft标准库开发人员之一对rvalue引用进行的精彩深入研究。

注意: MSDN上的链接文章(“ Rvalue引用:VC10中的C ++ 0x功能,第2部分”)是对Rvalue引用的非常清晰的介绍,但是对有关Rvalue引用的声明在C ++ 11草案中曾经是正确的。标准,但对于最后一个不是正确的!具体来说,它说在各个点上右值引用可以绑定到左值,这曾经是真实的,但是已经被更改。(例如int x; int && rrx = x;不再在GCC中编译)– drewbarbs 2014年7月13日在16:12

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

T&& r = T();

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

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

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

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

如果将此构造函数传递给临时对象,则不需要复制该副本,因为我们知道该临时对象将被销毁。为什么不利用已分配的临时资源?在C ++ 03中,由于无法确定是否已通过临时复制,因此无法阻止复制。在C ++ 11中,我们可以重载move构造函数:

foo(foo&& other)
{
   this->length = other.length;
   this->ptr = other.ptr;
   other.length = 0;
   other.ptr = nullptr;
}

注意这里的最大区别:move构造函数实际上是修改其参数。这将有效地将临时文件“移动”到正在构造的对象中,从而消除了不必要的复制。

move构造函数将用于临时函数和非const左值引用,这些引用已使用std::move函数显式转换为右值引用(它只是执行转换)。以下代码都为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"

完美的转发。右值引用使我们能够正确转发模板函数的参数。以这个工厂功能为例:

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

如果我们调用了该方法factory<foo>(5),则该参数将被推导为int&,即使foo的构造函数采用,也不会绑定到文字5 int。好吧,我们可以改用A1 const&,但是如果foo通过非const引用接受构造函数参数呢?为了使一个真正通用的工厂函数,我们就必须对超载工厂A1&A1 const&。如果factory采用1个参数类型,则可能会很好,但是每个其他参数类型都会将必需的重载设置乘以2。这很快就无法维护。

右值引用通过允许标准库定义std::forward可以正确转发左值/右值引用的函数来解决此问题。有关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)));
}

现在,当传递给T的构造函数时,参数的rvalue / lvalue-ness得以保留。这意味着,如果使用右值调用factory,则使用右值T调用的构造函数。如果使用左值调用factory,则使用左值调用T的构造函数。改进的工厂功能之所以有效,是因为以下一条特殊规则:

当函数参数类型的形式为T&&where T是模板参数,并且函数参数为type的左值时A,该类型A&用于模板参数推导。

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

auto p1 = factory<foo>(foo()); // calls foo(foo&&)
auto p2 = factory<foo>(*p1);   // calls foo(foo const&)

重要的右值参考属性

  • 对于重载解析,左值倾向于绑定到左值引用,而右值倾向于绑定到右值引用。因此,为什么临时人员比复制构造函数/赋值运算符更喜欢调用移动构造函数/移动赋值运算符。
  • 右值引用将隐式绑定到右值和作为隐式转换结果的临时对象。即float f = 0f; int&& i = f;格式正确,因为float可以隐式转换为int;该引用将是转换后的临时结果。
  • 命名的右值引用是左值。未命名的右值引用是右值。 了解为什么在以下std::move情况下必须进行调用非常重要:foo&& r = foo(); foo f = std::move(r);

65
为+1 Named rvalue references are lvalues. Unnamed rvalue references are rvalues.;在不知道这一点的情况下,我一直在努力理解为什么人们T &&t; std::move(t);长时间在移动控制器中这样做,等等。
legends2k

@MaximYegorushkin:在那个例子中,r绑定到一个纯rvalue(临时),因此临时对象的生存期应该延长,不是吗?
Peter Huene

@PeterHuene我认为,r值引用确实可以延长临时项的寿命。
Maxim Egorushkin

32
注意:MSDN上的链接文章(“ Rvalue引用:VC10中的C ++ 0x功能,第2部分”)对Rvalue引用的非常清晰的介绍,但是做出了有关Rvalue引用的声明,这些声明在C ++ 11草案中曾经是正确的。标准,但对于最后一个不是正确的!具体来说,它说在各个点上右值引用可以绑定到左值,这曾经是真实的,但是已被更改。(例如,int x; int &&rrx = x; 不再在GCC中进行编译)
drewbarbs 2014年

@PeterHuene在上面的示例中,不是typename identity<T>::type& a等效于T&吗?
ibp73 2014年

81

它表示右值参考。右值引用将仅绑定到临时对象,除非以其他方式明确生成。它们用于使对象在某些情况下更加高效,并提供一种称为“完美转发”的功能,该功能可以大大简化模板代码。

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

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

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

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

std::string s;
std::string& ref = s;
(std::string&)& anotherref = ref; // usually expressed via template

记不清C ++ 03所说的内容,但是在C ++ 0x中,处理右值引用时的结果类型至关重要。对类型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&&

考虑最简单的模板功能-最小和最大。在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);
}

我省去了返回类型推导,因为我不记得它是如何完成的,但是该min可以接受左值,右值,常量左值的任意组合。


你为什么用std::forward<A>(aref) < std::forward<B>(bref)?我认为当您尝试int&和尝试时这个定义是正确的float&。最好删除一种类型表单模板。
Yankes 2013年

25

T&& 当与类型推演(例如用于完美转发)一起使用时,术语“ 通俗地称为转发参考”。术语“通用引用”由Scott Meyers 在本文中提出,但后来被更改。

那是因为它可以是r值或l值。

例如:

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

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

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

// decltype
decltype(...)&& t = ...;

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


14

右值引用是一种行为,其行为与普通引用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&&)

那么什么是右值?任何不是左值的东西。左值是表示存储位置的表达式,它允许我们通过&运算符获取该存储位置的地址。

首先通过一个示例来了解右值可以完成的工作:

 #include <cstring>
 class Sample {
  int *ptr; // large block of memory
  int size;
 public:
  Sample(int sz=0) : ptr{sz != 0 ? new int[sz] : nullptr}, size{sz} 
  {
     if (ptr != nullptr) memset(ptr, 0, sz);
  }
  // copy constructor that takes lvalue 
  Sample(const Sample& s) : ptr{s.size != 0 ? new int[s.size] :\
      nullptr}, size{s.size}
  {
     if (ptr != nullptr) memcpy(ptr, s.ptr, 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
      size = s.size;

      if (size != 0) {
        ptr = new int[s.size];
        memcpy(ptr, s.ptr, s.size);
      } else 
         ptr = nullptr;
     }
     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
};     

构造函数和赋值运算符已被带有右值引用的版本重载。右值引用允许函数在编译时(通过重载解析)在条件“是否在左值或右值上调用我?”时分支。这使我们能够在上面创建更有效的构造函数和赋值运算符,从而移动资源而不是复制资源。

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

总结:右值引用允许移动语义(以及完美的转发,在下面的文章链接中讨论)。

一个容易理解的实用示例是类模板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, 

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

托马斯·贝克尔(Thomas Becker)的C ++ Rvalue References Explained是一篇出色的文章,其中包括很多很好的例子,对所有这些内容(例如,右值如何实现完美的转发以及这意味着什么)进行了解释。这篇文章主要依靠他的文章。

简短的介绍是Stroutrup等人撰写的“ 右值引用简介”。人


复制构造函数是否Sample(const Sample& s)还需要复制内容?对于“副本分配运算符”,存在相同的问题。
K.Karamazen

是的,你是对的。我无法复制内存。拷贝构造函数和拷贝赋值运算符都应该做的memcpy(PTR,s.ptr,大小)规模测试后= 0和默认构造函数应该做的memset(PTR,0,大小),如果大小= 0!
库尔特krueckeberg

好的谢谢。因此,可以删除此注释和前两个注释,因为该问题已在答案中得到纠正。
K.Karamazen
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.