What does T&& (double ampersand) mean in C++11?
我一直在研究C++ 11的一些新特性,我注意到其中一个是声明变量的双字节,比如EDCOX1(0)。
首先,这个野兽叫什么?我希望谷歌允许我们搜索这样的标点符号。
这到底是什么意思?
乍一看,它似乎是一个双引用(类似于C风格的双指针
它声明一个右值引用(标准建议文档)。
这里是对右值引用的介绍。
下面是微软标准库开发人员对右值引用的深入了解。
CAUTION: the linked article on MSDN ("Rvalue References: C++0x Features in VC10, Part 2") is a very clear introduction to Rvalue references, but makes statements about Rvalue references that were once true in the draft C++11 standard, but are not true for the final one! Specifically, it says at various points that rvalue references can bind to lvalues, which was once true, but was changed.(e.g. int x; int &&rrx = x; no longer compiles in GCC) – drewbarbs Jul 13 '14 at 16:12
C++ 03引用(现在称为C++ 11中的LValk引用)最大的区别在于它可以像临时一样绑定到一个rValt,而不必是const。因此,这种语法现在是合法的:
1 | T&& r = T(); |
右值引用主要提供以下内容:
移动语义。现在可以定义一个移动构造函数和移动赋值运算符,它采用右值引用而不是通常的常量左值引用。移动的功能与副本类似,只是它不必保持源不变;实际上,它通常修改源,使其不再拥有移动的资源。这对于消除无关副本非常有用,特别是在标准库实现中。
例如,复制构造函数可能如下所示:
1 2 3 4 5 6 | 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中,我们可以重载一个移动构造函数:
1 2 3 4 5 6 7 | foo(foo&& other) { this->length = other.length; this->ptr = other.ptr; other.length = 0; other.ptr = nullptr; } |
注意这里的大区别:move构造函数实际上修改了它的参数。这将有效地将临时对象"移动"到正在构造的对象中,从而消除不必要的副本。
move构造函数将用于临时引用和使用
1 2 | foo f1((foo())); // Move a temporary into f1; temporary becomes"empty" foo f2 = std::move(f1); // Move f1 into f2; f1 is now"empty" |
完美的转发。值引用允许我们正确地转发模板化函数的参数。以该工厂功能为例:
1 2 3 4 5 | template <typename T, typename A1> std::unique_ptr<T> factory(A1& a1) { return std::unique_ptr<T>(new T(a1)); } |
如果我们称为
rvalue引用通过允许标准库定义一个可以正确转发lvalue/rvalue引用的
这使我们能够像这样定义工厂功能:
1 2 3 4 5 | template <typename T, typename A1> std::unique_ptr<T> factory(A1&& a1) { return std::unique_ptr<T>(new T(std::forward<A1>(a1))); } |
现在,当传递给
When the function parameter type is of
the formT&& whereT is a template
parameter, and the function argument
is an lvalue of typeA , the typeA& is
used for template argument deduction.
因此,我们可以像这样使用工厂:
1 2 | auto p1 = factory<foo>(foo()); // calls foo(foo&&) auto p2 = factory<foo>(*p1); // calls foo(foo const&) |
重要的右值引用属性:
- 对于重载解析,lvalue更喜欢绑定到lvalue引用,rvalue更喜欢绑定到rvalue引用。因此,为什么临时性更喜欢调用移动构造函数/移动分配运算符而不是复制构造函数/分配运算符。
- 右值引用将隐式绑定到右值和作为隐式转换结果的临时值。也就是说,
float f = 0f; int&& i = f; 的格式很好,因为float可以隐式地转换为int;引用将是转换的结果的临时引用。 - 命名的右值引用是左值。未命名的右值引用是右值。这对于理解为什么在:
foo&& r = foo(); foo f = std::move(r); 中需要std::move 调用很重要。
它表示一个右值引用。除非另有明确生成,否则右值引用将只绑定到临时对象。它们用于使对象在某些情况下更高效,并提供一种称为完美转发的功能,这大大简化了模板代码。
在C++ 03中,你不能区分一个不可变的LValk的拷贝和一个rValk。
1 2 3 | 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中,情况并非如此。
1 2 3 | std::string s; std::string another(s); // calls std::string(const std::string&); std::string more(std::string(s)); // calls std::string(std::string&&); |
考虑这些构造函数背后的实现。在第一种情况下,字符串必须执行复制以保留值语义,这涉及到新的堆分配。但是,在第二种情况下,我们提前知道传递给构造函数的对象将立即被销毁,并且不必保持原样。在这种情况下,我们可以有效地交换内部指针,而根本不执行任何复制,这实际上更有效。移动语义有利于任何对内部引用资源进行昂贵或禁止复制的类。考虑EDOCX1 0的情况——现在我们的类可以区分临时和非临时性,我们可以使移动语义正确地工作,使得EDCOX1 1不能被复制但可以被移动,这意味着EDCOX1〔0〕可以合法存储在标准容器、排序等中,而C++ 03的EDCOX1引用3不能。
现在我们考虑使用右值引用的另一种方法-完美转发。考虑将引用绑定到引用的问题。
1 2 3 | std::string s; std::string& ref = s; (std::string&)& anotherref = ref; // usually expressed via template |
无法回忆起C++ 03关于这一点的说法,但是在C++ 0x中,处理rValk引用时的结果类型是至关重要的。对类型t的右值引用(其中t是引用类型)将成为类型t的引用。
1 2 3 4 | (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和unconst的组合手动重载。在C++ 0x中,它只是一个重载。与可变模板相结合,可以实现完美的转发。
1 2 3 4 5 6 7 8 9 | 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(bref)) return std::forward<A>(aref); else return std::forward(bref); } |
我取消了返回类型的扣除,因为我记不起它是如何立即完成的,但是Min可以接受lvalues、rvalues、const lvalues的任何组合。
当与类型演绎(例如用于完美转发)一起使用时,
这是因为它可能是r值或l值。
例子有:
1 2 3 4 5 6 7 8 9 10 11 12 | // template template<class T> foo(T&& t) { ... } // auto auto&& t = ...; // typedef typedef ... T; T&& t = ...; // decltype decltype(...)&& t = ...; |
在以下答案中可以找到更多的讨论:通用引用的语法
右值引用是一种行为类似于普通引用X&;的类型,但有几个例外。最重要的一点是,在函数重载解决方案方面,lvalues更喜欢旧式lvalue引用,而rvalues更喜欢新的rvalue引用:
1 2 3 4 5 6 7 8 | 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&&) |
那么什么是右值呢?任何不是左值的东西。一个左值一种表示内存位置的表达式,允许我们通过&;运算符获取该内存位置的地址。
首先,通过一个例子,我们更容易理解rvalues完成了什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | 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 "; } // 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 }; |
构造函数和赋值运算符已被采用右值引用的版本重载。右值引用允许函数在编译时(通过重载解析)在条件"是否对左值或右值调用?"。这允许我们在上面创建更高效的构造函数和赋值运算符,以便移动资源,而不是复制它们。
编译器在编译时自动进行分支(取决于它是为左值还是右值调用的),选择是否应调用移动构造函数或移动赋值运算符。
总结:右值引用允许移动语义(以及完美的转发,在下面的文章链接中讨论)。
一个易于理解的实用示例是类模板std::unique_ptr。由于唯一指针保持其基础原始指针的独占所有权,因此无法复制唯一指针。这将违背它们的专属所有权不变性。所以它们没有复制构造函数。但它们确实有移动构造函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 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: "; for(auto i = 0; i< 10; ++i) { cout << new_owner[i] <<","; } output: 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, |
1 2 | // first cast ptr from lvalue to rvalue unique_ptr<int[]> new_owner = TakeOwnershipAndAlter(std::move(ptr),0); |
一个很好的文章解释所有这些和更多(如rValm如何允许完美的转发,这意味着什么)有很多很好的例子是Thomas Becker的C++ RValk引用解释。这篇文章很大程度上依赖于他的文章。
简短的介绍是stroutrup等人对右值引用的简要介绍。