T &&(双和号)在C ++ 11中意味着什么?
我一直在研究C ++ 11的一些新特性,我注意到了在声明variables中的双和号,比如T&& var
。
一开始,这个野兽叫什么名字? 我希望Google允许我们search这样的标点符号。
究竟是什么意思?
乍一看,它似乎是一个双引用(就像C风格的双指针T** var
),但是我很难想到这个用例。
它声明了一个右值引用 (标准提案doc)。
这里是右值引用的介绍。
以下是Microsoft标准库开发人员对rvalue引用的深入了解。 (但在阅读本文之前,请参阅此答案后的注释中的注意事项。)
C ++ 03引用(现在称为C ++ 11中的左值引用)最大的区别在于,它可以像临时值那样绑定到右值,而不必是const。 因此,这个语法现在是合法的:
T&& r = T();
右值引用主要提供以下内容:
移动语义 。 移动构造函数和移动赋值运算符现在可以被定义为取右值引用,而不是通常的const-lvalue引用。 移动function就像一个副本,除非它不必保持源文件不变; 实际上,它通常会修改源,使其不再拥有移动的资源。 这对消除多余的副本非常有用,特别是在标准库实现中。
例如,复制构造函数可能如下所示:
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; }
移动构造函数实际上修改了它的参数。 这将有效地将临时“移动”到正在构build的对象中,从而消除不必要的副本。
移动构造函数将用于临时和非常量左值引用,它们使用std::move
函数(它只是执行转换)显式转换为右值引用。 以下代码都调用f1
和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"
完美的转发 。 右值引用允许我们正确地为模板函数转发参数。 以此工厂function为例:
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个参数types,这可能没有问题,但是每个额外的参数types都会将必要的重载乘以2。这很快就不能维持。
右值引用通过允许标准库定义一个可正确转发左值/右值引用的std::forward
函数来解决这个问题。 有关std::forward
如何工作的更多信息,请参阅这个出色的答案 。
这使我们能够像这样定义工厂function:
template <typename T, typename A1> std::unique_ptr<T> factory(A1&& a1) { return std::unique_ptr<T>(new T(std::forward<A1>(a1))); }
现在参数的右值/左值被传递给T
的构造函数。 这意味着如果工厂被右值调用,则T
的构造函数被右值调用。 如果工厂被左值调用, T
的构造函数被调用左值。 改进的工厂function因为一个特殊的规则而起作用:
当函数参数types的forms为
T&&
,其中T
是模板参数,而函数参数是A
types的左值时,typesA&
被用于模板参数推导。
因此,我们可以这样使用工厂:
auto p1 = factory<foo>(foo()); // calls foo(foo&&) auto p2 = factory<foo>(*p1); // calls foo(foo const&)
重要的右值引用属性 :
- 对于重载parsing, 左值更喜欢绑定左值引用,右值更喜欢绑定右值引用 。 因此为什么临时variables更喜欢在复制构造函数/赋值运算符上调用移动构造函数/移动赋值运算符。
- 右值引用将隐含地绑定到右值和作为隐式转换结果的临时值 。 即
float f = 0f; int&& i = f;
float f = 0f; int&& i = f;
forms良好,因为float可以隐式转换为int; 该参考将是一个临时的,这是转换的结果。 - 命名的右值引用是左值。 未命名的右值引用是右值。 这对了解
std::move
调用为什么是必须的:foo&& r = foo(); foo f = std::move(r);
foo&& r = foo(); foo f = std::move(r);
它表示一个右值引用。 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&);
在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&&);
考虑这些构造函数的实现。 在第一种情况下,string必须执行副本以保留值语义,这涉及到新的堆分配。 然而,在第二种情况下,我们事先知道传入我们的构造函数的对象是立即破坏的,并且不必保持不变。 在这种情况下,我们可以有效地交换内部指针而不执行任何复制,这实际上更有效。 移动语义有利于任何昂贵的或禁止复制内部引用资源的类。 考虑一下std::unique_ptr
的情况 – 现在我们的类可以区分临时对象和非临时对象,我们可以使移动语义正确工作,这样unique_ptr
就不能被复制,而是可以被移动,这意味着std::unique_ptr
可以合法存储在标准容器,sorting等,而C + + 03的std::auto_ptr
不能。
现在我们考虑右值引用的其他用法 – 完美转发。 考虑将引用绑定到引用的问题。
std::string s; std::string& ref = s; (std::string&)& anotherref = ref; // usually expressed via template
无法回想C ++ 03对此所说的内容,但在C ++ 0x中,处理右值引用时的结果types至关重要。 对typesT的右值引用,其中T是引用types,成为typesT的引用。
(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和non-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可以接受任何左值,右值,常值左值的组合。
T&&
与types扣除 (如完美转发) 一起使用的术语俗称为通用参考 。 这是由Scott Meyers 在这篇文章中创造的。
那是因为它可能是r值或l值。
例子是:
// template template<class T> foo(T&& t) { ... } // auto auto&& t = ...; // typedef typedef ... T; T&& t = ...; // decltype decltype(...)&& t = ...;
请注意,标准本身没有这个概念,它只是讨论参考折叠规则,引用types推导和&&语法的(单数)组合的一种方法。
更多的讨论可以在以下答案中find: 通用引用的语法
右值引用是一种类似于普通引用X的types,除了几个例外。 最重要的是,当涉及到函数重载parsing时,左值更喜欢老式的左值引用,而右值更喜欢新的右值引用:
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&&)
那么什么是右值? 任何不是左值的东西。 左值是一个expression式,指的是一个内存位置,并允许我们通过&运算符来获取该内存位置的地址。
首先要比较容易理解一个例子:
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 };
构造函数和赋值操作符已经被重载的版本取右值引用。 右值引用允许函数在编译时(通过重载parsing)在“我被调用左值还是右值?”的条件下进行分支。 这使我们能够创build更有效率的构造函数和赋值操作符,以便移动资源而不是复制它们。
编译器自动在编译时分支(取决于是左值还是右值调用),select是否调用移动构造函数或移动赋值操作符。
总结:右值引用允许移动语义(和完美的转发,在下面的文章链接讨论)。
一个实用的易于理解的例子是类模板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”提供了一篇很好的文章,解释了所有这些和更多的内容(比如rvalues是如何实现完美的转发以及这是什么意思)。 这篇文章严重依赖他的文章。
一个简短的介绍是由Stroutrup,R。 人