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

我一直在研究C++11的一些新特性,我注意到一个是声明变量时的双与号,比如T&& var

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

它到底是什么意思?

乍一看,它似乎是一个双引用(就像C风格的双指针T** var),但我很难想到它的用例。

301234 次浏览

它声明了一个右值参考(标准提案文档)。

这是对右值参考文献的介绍。

下面是对Microsoft标准库开发之一的右值引用的深入了解。

注意:MSDN上的链接文章(“Rvalue引用:VC10中的C++0x功能,第2部分”)是对Rvalue引用的非常清晰的介绍,但是对Rvalue引用的陈述在C++11标准草案中曾经是正确的,但在最终标准中不是正确的!具体来说,它在不同的点上说右值引用可以绑定到左值,这曾经是正确的,但被改变了。(例如int x; int&&rrx=x;不再在GCC中编译)-drewbarbs Jul 13'14 at 16:12

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

T&& r = T();

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

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

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

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中,我们可以重载一个移动构造函数:

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

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

移动构造函数将用于临时变量和非const左值引用,这些引用使用std::move函数显式转换为右值引用(它只是执行转换)。以下代码都调用f1f2的移动构造函数:

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的构造函数采用int,它也不会绑定到文字5。好吧,我们可以使用A1 const&,但是如果foo通过非const引用获取构造函数参数怎么办?为了制作一个真正通用的工厂函数,我们必须在A1&A1 const&上重载工厂。如果工厂采用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)));}

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

当函数参数类型为形式T&&,其中T是模板参数和函数参数是类型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;格式良好,因为浮点数可隐式转换为int;引用将是转换结果的临时值。
  • 命名的右值引用是左值。未命名的右值引用是右值。这对于理解为什么在以下情况下需要std::move调用很重要:foo&& r = foo(); foo f = std::move(r);

它表示一个右值引用。右值引用只会绑定到临时对象,除非另有明确生成。它们用于在某些情况下提高对象的效率,并提供一种称为完美转发的工具,这大大简化了模板代码。

在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的情况-现在我们的类可以区分临时和非临时,我们可以使移动语义学正确工作,以便unique_ptr不能被复制,但可以被移动,这意味着std::unique_ptr可以合法地存储在标准容器中,排序等,而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&&

考虑最简单的模板函数-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);elsereturn std::forward<B>(bref);}

我省略了返回类型推导,因为我不记得它是如何即刻完成的,但是min可以接受左值、右值、const左值的任何组合。

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

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

例如:

// templatetemplate<class T> foo(T&& t) { ... }
// autoauto&& t = ...;
// typedeftypedef ... T;T&& t = ...;
// decltypedecltype(...)&& t = ...;

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

右值引用是一种行为与普通引用X&非常相似的类型,但有几个例外。最重要的是,当涉及到函数重载解决时,左值更喜欢老式的左值引用,而右值更喜欢新的右值引用:

void foo(X& x);  // lvalue reference overloadvoid 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 memoryint 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 lvalueSample(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 rvalueSample(Sample&& s){  // steal s's resourcesptr = s.ptr;size = s.size;s.ptr = nullptr; // destructive writes.size = 0;cout << "Move constructor called on rvalue." << std::endl;}// normal copy assignment operator taking lvalueSample& operator=(const Sample& s){if(this != &s) {delete [] ptr; // free current pointersize = s.size;
if (size != 0) {ptr = new int[s.size];memcpy(ptr, s.ptr, s.size);} elseptr = nullptr;}cout << "Copy Assignment called on lvalue." << std::endl;return *this;}// overloaded move assignment operator taking rvalueSample& operator=(Sample&& lhs){if(this != &s) {delete [] ptr; //don't let ptr be orphanedptr = lhs.ptr;   //but now "steal" lhs, don't clone it.size = lhs.size;lhs.ptr = nullptr; // lhs's new "stolen" statelhs.size = 0;}cout << "Move Assignment called on rvalue" << std::endl;return *this;}//...snip};

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

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

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

一个实用的易于理解的例子是类模板圣d::unique_ptr。由于unique_ptr维护其底层原始指针的独占所有权,unique_ptr不能被复制。这将违反它们独占所有权的不变量。所以它们没有复制构造函数。但它们有移动构造函数:

template<class T> class unique_ptr {//...snipunique_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 rvaluestd::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 functionunique_ptr<int[]> ptr{new int[10]};
// first cast ptr from lvalue to rvalueunique_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)通常使用d::移动完成

// first cast ptr from lvalue to rvalueunique_ptr<int[]> new_owner = TakeOwnershipAndAlter(std::move(ptr),0);

一篇很好的文章解释了所有这些以及更多(比如右值如何允许完美转发以及这意味着什么),其中有很多很好的例子是Thomas Becker的C++参考说明

一个较短的介绍是Stroutrup等人的右值引用简介