我一直在研究c++ 11的一些新特性,我注意到在声明变量时使用了双&号,比如t&var。
首先,这只野兽叫什么?我希望谷歌能让我们像这样搜索标点符号。
它到底是什么意思?
乍一看,它似乎是一个双重引用(就像c风格的双指针T** var),但我很难想到它的用例。
我一直在研究c++ 11的一些新特性,我注意到在声明变量时使用了双&号,比如t&var。
首先,这只野兽叫什么?我希望谷歌能让我们像这样搜索标点符号。
它到底是什么意思?
乍一看,它似乎是一个双重引用(就像c风格的双指针T** var),但我很难想到它的用例。
它声明了一个右值引用(标准提案文档)。
下面是对右值引用的介绍。
下面是微软标准库开发人员对右值引用的深入研究。
注意:MSDN上的链接文章(“右值引用:VC10中的c++ 0x特性,第2部分”)非常清楚地介绍了右值引用,但其中关于右值引用的陈述在c++ 11标准草案中曾经是正确的,但在最终的标准中却不是正确的!具体来说,它说在不同的点右值引用可以绑定到左值,这曾经是真的,但被改变了。int x;Int &&rrx = x;不再在GCC中编译)- drewbarbs july 13 '14 at 16:12
c++ 03引用(现在在c++ 11中称为左值引用)之间最大的区别是,它可以像临时对象一样绑定到右值,而不必为const。因此,这个语法现在是合法的:
T&& r = T();
右值引用主要提供以下功能:
移动语义。现在可以定义一个move构造函数和move赋值操作符,它们接受一个右值引用,而不是通常的const-lvalue引用。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和f2的move构造函数:
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));
}
If we called factory<foo>(5), the argument will be deduced to be int&, which will not bind to a literal 5, even if foo's constructor takes an int. Well, we could instead use A1 const&, but what if foo takes the constructor argument by non-const reference? To make a truly generic factory function, we would have to overload factory on A1& and on A1 const&. That might be fine if factory takes 1 parameter type, but each additional parameter type would multiply the necessary overload set by 2. That's very quickly unmaintainable.
右值引用通过允许标准库定义一个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的构造函数时被保留。这意味着如果使用右值调用factory,则使用右值调用T的构造函数。如果使用左值调用factory,则使用左值调用T的构造函数。改进后的工厂功能工作是因为一个特殊的规则:
当函数参数类型为 表单T&&,其中T是模板 形参和函数实参 是类型A的左值,类型A&是 用于模板参数推断。
因此,我们可以像这样使用factory:
auto p1 = factory<foo>(foo()); // calls foo(foo&&)
auto p2 = factory<foo>(*p1); // calls foo(foo const&)
重要的右值引用属性:
For overload resolution, lvalues prefer binding to lvalue references and rvalues prefer binding to rvalue references. Hence why temporaries prefer invoking a move constructor / move assignment operator over a copy constructor / assignment operator. rvalue references will implicitly bind to rvalues and to temporaries that are the result of an implicit conversion. i.e. float f = 0f; int&& i = f; is well formed because float is implicitly convertible to int; the reference would be to a temporary that is the result of the conversion. Named rvalue references are lvalues. Unnamed rvalue references are rvalues. This is important to understand why the std::move call is necessary in: 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&&);
Consider the implementation behind these constructors. In the first case, the string has to perform a copy to retain value semantics, which involves a new heap allocation. However, in the second case, we know in advance that the object which was passed in to our constructor is immediately due for destruction, and it doesn't have to remain untouched. We can effectively just swap the internal pointers and not perform any copying at all in this scenario, which is substantially more efficient. Move semantics benefit any class which has expensive or prohibited copying of internally referenced resources. Consider the case of std::unique_ptr- now that our class can distinguish between temporaries and non-temporaries, we can make the move semantics work correctly so that the unique_ptr cannot be copied but can be moved, which means that std::unique_ptr can be legally stored in Standard containers, sorted, etc, whereas C++03's std::auto_ptr cannot.
现在我们考虑右值引用的另一种用法——完全转发。考虑将引用绑定到引用的问题。
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);
else
return std::forward<B>(bref);
}
我省略了返回类型推断,因为我不记得它是怎么做的,但是min可以接受任何左值的组合,右值,const左值。
当与类型演绎一起使用时(例如用于完全转发),术语T&&被通俗地称为转发引用。术语“普遍参考”是由Scott Meyers在这篇文章中创造的,但后来被更改了。
这是因为它可能是r值也可能是l值。
例子有:
// template
template<class T> foo(T&& t) { ... }
// auto
auto&& t = ...;
// typedef
typedef ... T;
T&& t = ...;
// decltype
decltype(...)&& t = ...;
更多的讨论可以在以下答案中找到:通用引用的语法
右值引用是一种行为与普通引用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的指针不能被复制。这将违反他们的独家所有权不变原则。所以它们没有复制构造函数。但是它们有move构造函数:
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++右值参考解释》(c++ Rvalue References Explained)是一篇很好的文章,解释了所有这些内容(比如右值如何允许完全转发以及这意味着什么),并给出了很多很好的例子。这篇文章很大程度上依赖于他的文章。
一个简短的介绍是stroutup等人的《右值引用简介》