引子
C++中提供了两种引用方式。左值引用与右值引用。
其中右值引用是C++11的新标准添加的内容。
所谓的右值引用就是必须绑定到右值的引用。
在介绍之前,先说明一下C++中的左值和右值的规定。
实际上,最早在C语言中就有了左值和右值之分。最初的左值即指在赋值号左边的变量,右值指在赋值号右边的变量。随着C语言的发展和C++的出现,重新定义了左值和右值的概念。
可寻址的表达式是左值,不可寻址的表达式是右值。
这是最为方便的区分左值和右值的方法,举几个例子:
#include <iostream>
using namespace std;
int main()
{
int x = 3 + 4;
cout << x << endl;
}
这段代码中,x是可寻址的,但是3+4是不可寻址的,所以x是一个左值,3+4是一个右值表达式。
(一般而言,一个左值表达式表示的是一个对象的身份,一个右值表达式表示的是对象的值)
更多的一般性的例子:
int main()
{
int i, j, *p;
// Correct usage: the variable i is an lvalue.
i = 7;
// Incorrect usage: The left operand must be an lvalue (C2106).
7 = i; // C2106
j * 4 = 7; // C2106
// Correct usage: the dereferenced pointer is an lvalue.
*p = i;
const int ci = 7;
// Incorrect usage: the variable is a non-modifiable lvalue (C3892).
ci = 9; // C3892
// Correct usage: the conditional operator returns an lvalue.
((i < 3) ? i : j) = 7;
*p = a + b;
}
这里需要稍微解释一下最后一个表达式。
*p是一个左值,因为可寻址。a和b都是一个左值,但是它们的和是不可寻址的,所以a+b是一个右值表达式。
实际上,更多的右值的例子是临时对象。
还是以*p = a + b
为例,a+b得到一个值返回一个临时对象,临时对象是不可寻址的。
这也可以很好的解释下面的这个例子:
int a = 1;
cout << &(++a) << endl;
cout << &(a++) << endl;
a++是先返回一个a的拷贝,是一个临时对象,所以这是一个右值,不可取址,这样编译器会提示错误。
++a是直接将a增加1后返回左值引用,是可寻址的。
同理,因为许多函数的返回对象都是临时对象,所以均为不可寻址的右值。
int Max(int x, int y)
{
return x > y ? x : y;
}
这里虽然x和y传递进来都是左值,但是返回值却是一个临时对象,不可寻址,所以是右值。
为什么要加入右值引用?
右值引用的加入是为了实现移动语义。
若要更好地了解移动语义,思考下面这个例子。
#include <iostream>
using namespace std;
vector<int> doubleValues (const vector<int>& v)
{
vector<int> new_values( v.size() );
for (auto itr = new_values.begin(), end_itr = new_values.end(); itr != end_itr; ++itr )
{
new_values.push_back( 2 * *itr );
}
return new_values;
}
int main()
{
vector<int> v;
for ( int i = 0; i < 100; i++ )
{
v.push_back( i );
}
v = doubleValues( v );
}
在doubleValues的调用中,会发生两次拷贝。
第一次是函数体重的new_values
第二次是v = doubleValues(v)的operator=()
更可气的是在赋值完毕后临时对象又被销毁了,大大浪费内存和时间。
有一种避免多次拷贝的办法是返回一个引用,不过这样还是需要在函数中动态分配内存,但是C++要求尽量简化操作,不分配内存。
这时候,移动语义就派上用场了,直接移动元素,避免多次内存分配销毁,大大提高了效率。
右值引用能够实现移动语义是因为右值的重要性质——只能绑定到一个将要销毁的对象。而且右值引用有一点和右值需要区别开来,就是右值引用是可取址的:
int && r = 3;
cout << &r << endl;
这样可行的原因是,编译器将已命名的右值引用视为左值。当右值引用出现在函数的参数列表中时,也会发生此类情况。
右值引用的使用
右值引用同左值引用一样,必须在定义的时候进行初始化。以&&
为右值引用的标识符。
int fun(int x)
{
return x * 3;
}
int && r1 = 3 + 4 + 7;
int && r2 = fun(3);
int && r3 = r1 + r2;//r1+r2产生临时对象是一个右值
const int & cl1 = fun(3);//const左值引用也可以绑定一个右值!
const int & cl2 = r3;
cout << "r1 : " << r1 << endl;
cout << "r2 : " << r2 << endl;
cout << "r3 : " << r3 << endl;
cout << "cl1 : " << cl1 << endl;
cout << "cl2 : " << cl2 << endl;
再强调一遍,编译器将已命名的右值引用视为左值。
显然我们不能直接将右值引用绑定到一个左值上,但是利用标准库函数std::move(obj)
我们可以将一个左值(obj)转换为对应的右值引用。
int && r = std::move(lerf_value);
move函数告诉编译器,我这里有一个左值,但是我希望像一个右值一样(短暂存在,右值引用的对象即将被销毁,该对象没有其他用户使用)处理它。这意味着,当使用move函数时,我们对编译器承诺除了赋值和销毁之外,我们不会再使用它。
并且,在调用move之后,我们不能对“移后源”(obj)的值做出任何假设。由于移后源的这种不确定状态,使用move是危险的,一定要确保obj没有其他的用户!
实际上,move就是static_cast<typename &&>(obj)
。
移动构造函数与移动赋值运算符
前面讲到加入移动语义是为了更快的将元素直接移动到新的内存当中从而减少拷贝次数,提升程序的性能。
那么给出MSDN的例子,说明如何编写移动构造函数。
// MemoryBlock.h
#pragma once
#include <iostream>
#include <algorithm>
class MemoryBlock
{
public:
// Simple constructor that initializes the resource.
explicit MemoryBlock(size_t length)
: _length(length)
, _data(new int[length])
{
std::cout << "In MemoryBlock(size_t). length = "
<< _length << "." << std::endl;
}
// Destructor.
~MemoryBlock()
{
std::cout << "In ~MemoryBlock(). length = "
<< _length << ".";
if (_data != NULL)
{
std::cout << " Deleting resource.";
// Delete the resource.
delete[] _data;
}
std::cout << std::endl;
}
// Copy constructor.
MemoryBlock(const MemoryBlock& other)
: _length(other._length)
, _data(new int[other._length])
{
std::cout << "In MemoryBlock(const MemoryBlock&). length = "
<< other._length << ". Copying resource." << std::endl;
std::copy(other._data, other._data + _length, _data);
}
// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
std::cout << "In operator=(const MemoryBlock&). length = "
<< other._length << ". Copying resource." << std::endl;
if (this != &other)
{
// Free the existing resource.
delete[] _data;
_length = other._length;
_data = new int[_length];
std::copy(other._data, other._data + _length, _data);
}
return *this;
}
// Retrieves the length of the data resource.
size_t Length() const
{
return _length;
}
private:
size_t _length; // The length of the resource.
int* _data; // The resource.
};
这是未添加移动构造函数版本的。
移动构造函数与移动赋值运算符他们的类型都是右值引用(不是const)。
// Move constructor.
MemoryBlock(MemoryBlock&& other)
: _data(NULL)
, _length(0)
{
std::cout << "In MemoryBlock(MemoryBlock&&). length = "
<< other._length << ". Moving resource." << std::endl;
// Copy the data pointer and its length from the
// source object.
_data = other._data;
_length = other._length;
// Release the data pointer from the source object so that
// the destructor does not free the memory multiple times.
other._data = NULL;
other._length = 0;
}
移动构造函数不分配任何新内存,直接接管给定的对象中的内存资源。在接管后,保证移后源的各数据成员都处于一种可析构的状态。如果是指针则赋值为空防止多次析构。
// Move assignment operator.
MemoryBlock& operator=(MemoryBlock&& other)
{
std::cout << "In operator=(MemoryBlock&&). length = "
<< other._length << "." << std::endl;
if (this != &other)
{
// Free the existing resource.
delete[] _data;
// Copy the data pointer and its length from the
// source object.
_data = other._data;
_length = other._length;
// Release the data pointer from the source object so that
// the destructor does not free the memory multiple times.
other._data = NULL;
other._length = 0;
}
return *this;
}
移动赋值构造函数同理。
移动构造与拷贝构造
编译器也会合成默认的移动构造函数和移动赋值运算符,不过,合成条件与拷贝构造函数大不相同。
只要一个类定义了自己的copy构造函数,copy assign运算符或者析构函数,编译器就不会合成默认的移动构造函数和移动赋值运算符。
不过,很有意思的是,如果没有移动构造函数的情况下,还是使用了std::move()函数,编译是可以通过的。但是此时使用的是copy构造函数,这是因为&&可以隐式转换为const &.
例如:
class Foo
{
public:
Foo() = default;
Foo(const Foo &) { cout << "const Foo& called" << endl; }
//没有定义移动构造函数,也不会生成
};
int main()
{
Foo x;
Foo y(std::move(x));//调用copy构造函数
return 0;
}