目录
1. 按值传递
什么是按值传递?
当一个函数通过值的方式获取它的参数时,就会包含一个拷贝的动作,编译器知道如何去进行拷贝。如果参数是自定义类型,则我们还需要提供拷贝构造函数,或者赋值运算符来进行深拷贝。
然而,拷贝是需要代价的。在我们使用STL容器时,就存在大量的拷贝代价。当按值传递参数时,会产生临时对象,浪费宝贵的CPU以及内存资源。
需要找到一个减少不必要拷贝的方法。移动语义就是其中一种。
2. 右值引用
此处介绍右值引用的目的,是为了实现后面的移动语义。
右值引用可以帮助我们分辨一个值是左值还是右值。C++11标准规定右值引用只能绑定右值,它使用两个&符号进行声明。
int&& rvalue_ref = 99;
下面是一个右值引用例子:
#include<iostream>
void f(int& i) {
std::cout<<"lvalue ref: "<<i<<std::endl;
}
void f(int&& i) {
std::cout<<"rvalue ref: "<<i<<std::endl;
}
int main()
{
int i=99;
f(i); //lvalue ref called
f(99); //rvalue ref called
f(std::move(i)); //rvalue ref called
return 0;
}
运行结果为:
lvalue ref: 99
rvalue ref: 99
rvalue ref: 99
其中std::move函数的作用就是将一个左值转换为成右值。
如果一个表达式会生成一个临时对象,则它就是一个右值。如下:
#include<iostream>
int getValue() {
int i = 22;
return i;
}
int main() {
std::cout<<getValue()<<std::endl;
return 0;
}
getValue()就是一个右值。注意:返回的这个值并不是i的引用,而是一个临时值。
在C++0x中,使用左值引用仅能绑定const类型的临时对象。
const int& val = getValue(); //OK
int& val = getValue(); //Wrong!
但是,C++11中的右值引用允许我们绑定一个mutable引用到rvalue,但不是lvalue。换言之,右值引用可以完美的判断一个值是否为临时对象。
const int&& val = getValue(); //OK
int&& val = getValue(); //OK
下面进行一下比较:
void printReference (const int& value)
{
std::cout << value;
}
void printReference (int&& value)
{
std::cout << value;
}
第一个函数的参数类型为const lvalue, 它可以接收任何传入参数,无论是rvalue还是lvalue. 但是,第二个函数只能接收右值引用。
换言之,我们可以使用函数重载,即一个使用左值引用参数,另一个使用右值引用参数,来判断传入的参数是左值还是右值。也就是说,C++11引入了一个新的类型, non-const reference, 即右值引用,声明方式为T&&。它代表一个初始化后还允许被修改的临时值。这也是移动语义的基础。
#include<iostream>
void printRef(int& value) {
std::cout<<"lvalue: value = "<<value<<std::endl;
}
void printRef(int&& value) {
std::cout<<"rvalue: value = "<<value<<std::endl;
}
int getVal() {
int tmp = 88;
return tmp;
}
int main(){
int i = 11;
printRef(i);
printRef(getVal()); //printRef(88);
return 0;
}
运行结果为:
lvalue: value = 11
rvalue: value = 88
注意:第一个printRef函数中,参数没有const修饰符,这样使得它只能接受左值。
到此为止,我们可以写出两个区别明显的重载函数:一个只接受左值参数,另一个只接受右值参数。有何好处呢?它给予了我们一种以更少代码实现更有效率程序的方法!
对右值引用的总结:
1)int&& a: C++11中的新类型-右值引用采用的声明方式
2)non-const 左值引用绑定到一个对象
3)右值引用绑定到一个通常不会再被使用的临时对象
3. 移动语义
在C++03中,如果参数按值传递,就会隐含一个不必要的深拷贝代价在里边。我们可以使用右值引用来避免深拷贝带来的性能损失。
基于前面的论述,我们已经有了一种可以用来判断是临时对象还是永久对象的方法。现在的问题是,如何使用它呢?
右值引用的主要作用就是用来创建移动构造函数(move constructor)以及移动赋值运算符(move assignment operator)。移动构造函数,类似于拷贝构造函数,使用对象实例做为参数并且基于原始对象来创建一个新的实例。只是,移动构造函数可以避免内存重新分配,因为我们知道它提供了一个临时对象。
换言之,右值引用和移动语义避免了不必要的临时对象拷贝,我们无需拷贝临时对象。这样,临时对象所需的资源,可以用于其它对象。
右值通常是临时的并且可以被修改的。如果我们知道函数参数是一个右值,可以把它当作临时存储使用,或者获取其内容,而不会改变程序的输出。这意味着我们可以移动其内容,而无需拷贝其内容。这样节省了大量的内容分配,并可对大量动态内存结构程序进行优化。
下面是一个典型的使用移动语义的类定义:
#include<iostream>
#include<algorithm>
#include<vector>
class Dummy {
public:
explicit Dummy(size_t length):_length(length), _data(new int[length]) {
std::cout<<"Dummy(size_t).length = "<<_length<<"."<<std::endl;
}
~Dummy() {
std::cout<<"~Dummy().length = "<<_length<<"."<<std::endl;
if(_data!=NULL){
std::cout<<"delete resource."<<std::endl;
delete _data;
}
}
Dummy(const Dummy&other):_length(other._length),_data(new int[other._length]) {
std::cout<<"Dummy(const &Dummy).length = "<<other._length<<".Copying resource."<<std::endl;
std::copy(other._data, other._data+_length, _data);
}
//赋值运算符
Dummy& operator=(const Dummy& other) {
std::cout<<"operator=(const Dummy&).length = "<<other._length<<". Copying resource."<<std::endl;
if(this != &other) {
delete _data;
_length = other._length;
_data = new int[_length];
std::copy(other._data, other._data+_length, _data);
}
return *this;
}
//移动语义的构造函数
Dummy(Dummy&& other):_length(0),_data(NULL) {
std::cout<<"Dummy(Dummy&&).length = "<<other._length<<". Moving resource."<<std::endl;
//从源对象复制数据指针以及数据长度
_data = other._data;
_length = other._length;
//修改源对象中的数据指针,这样析构的时候不会多次释放内存。
other._data = NULL;
other._length = 0;
}
//移动语义的赋值运算符
Dummy& operator=(Dummy&& other) {
std::cout<<"operator=(Dummy&&).length = "<<other._length<<"."<<std::endl;
if(this != &other) {
delete _data;
//从源对象复制数据指针以及数据长度
_data = other._data;
_length = other._length;
//修改源对象中的数据指针,这样析构的时候不会多次释放内存。
other._data = NULL;
other._length = 0;
}
return *this;
}
private:
size_t _length;
int* _data;
};
int main() {
std::vector<Dummy> vec;
vec.push_back(Dummy(55));
vec.push_back(Dummy(77));
//插入一个新的元素至vector的第二个位置。
vec.insert(vec.begin()+1, Dummy(66));
return 0;
}
运行结果为:
Dummy(size_t).length = 55.
Dummy(Dummy&&).length = 55. Moving resource.
~Dummy().length = 0.
Dummy(size_t).length = 77.
Dummy(Dummy&&).length = 77. Moving resource.
Dummy(const &Dummy).length = 55.Copying resource.
~Dummy().length = 55.
delete resource.
~Dummy().length = 0.
Dummy(size_t).length = 66.
Dummy(Dummy&&).length = 66. Moving resource.
Dummy(const &Dummy).length = 55.Copying resource.
Dummy(const &Dummy).length = 77.Copying resource.
~Dummy().length = 55.
delete resource.
~Dummy().length = 77.
delete resource.
~Dummy().length = 0.
~Dummy().length = 55.
delete resource.
~Dummy().length = 66.
delete resource.
~Dummy().length = 77.
delete resource.
4. 移动构造函数
下面是一个最简单的移动构造函数:
Dummy(Dummy&& other) noexcept // C++11 - specify non-exception throwing function
{
_date = other._data; // shallow copy
other._date = nullptr;
}
注意:上面函数没有分配任何新的资源,只是将内容移动了而不是拷贝: other中的内容移到了一个新成员里边,然后other中的内容被清除了。它占用了other的资源并且将other设置为了默认构造函数时的状态。最重要的点是没有内容的分配开销。我们只是分配了一个地址,只需很少的几个机器指令即可实现。
假设这个地址指向的是包含上万个整数的数组,我们无需拷贝其中的元素,无需创建新的东西,而只是移动了它们。如果使用旧的拷贝构造函数,且这个类有一个拥有上万个元素的成员数组,则我们需要大量的赋值操作,代价很大。现在,有了移动构造函数之后,可以节省很多。
移动构造函数比拷贝构造函数快很多,因为它既不分配内存,也不拷贝内存块。
5. 移动赋值运算符
一个简单的移动赋值运算符如下:
Dummy& operator=(Dummy&& other) noexcept
{
_data = other._data;
other._data = nullptr;
return *this;
}
移动赋值运算符与拷贝构造函数类似,除了转移源object之前,它会释放object所拥有的资源。步骤如下:
1). 释放*this所拥有的资源
2). 转移other的资源
3). 将other设置为默认状态
4). 返回*this
5). 结果分析
因为C++11支持右值引用,这样std::vector::push_back()函数相当于有两个版本:一个像以前一样接受左值参数const T&, 另外一个新的接受右值参数T&&。
main()函数中调用了两次push_back来插入到vector中:
std::vector<A> vec;
vec.push_back(A(55));
vec.push_back(A(77));
这两个push_back,实际上都使用的push_back(T&&), 因为传入的参数是右值。push_back(T&&)会将参数中的资源,移动到vector中的对象A, 使用的是A的移动构造函数。在C++03中,相同的这段代码会进行参数的赋值,因为会调用参数的拷贝构造函数。
如果传入的参数是一个左值,则push_back(const T&)会被调用:
std::vector<A> vec;
A obj(25); //lvalue
vec.push_back(obj); //push_back(const T&)
不过,我们可以使用static_cast将左值引用转换为右值引用,使得调用的是push_back(T&&)。
// calls push_back(T&&)
vec.push_back(static_cast<A&&>(obj));
另外一种办法是,使用std::move()来实现:
// calls push_back(T&&)
vec.push_back(std::move(obj));
总结起来,push_back(T&&)看似总是最优选择,因为它减少了不必要的拷贝开销。
然而,需要注意的是push_back(T&&)总会清空传入的参数。如果需要在执行一个push_back()操作后,仍然保持参数原始状态,则还是需要选择拷贝语义(拷贝构造函数),而不是移动语义。
6. 使用move()交换对象
下面例子显示如何使用move来交互对象
#include<iostream>
class Dummy
{
public:
//constructor
explicit Dummy(size_t length):_length(length),_data(new int[length]) { }
//move constructor
Dummy(Dummy&& other) {
_data = other._data;
_length = other._length;
other._data = nullptr;
other._length = 0;
}
//move assignment operator
Dummy& operator= (Dummy&& other) noexcept {
_data = other._data;
_length = other._length;
other._data = nullptr;
other._length = 0;
return *this;
}
void swap(Dummy& other) {
Dummy tmp = std::move(other);
other = std::move(*this);
*this = std::move(tmp);
}
int getLength() { return _length; }
int* getData() { return _data; }
private:
size_t _length;
int* _data;
};
int main()
{
Dummy a(11),b(22);
std::cout<<a.getLength()<<" "<<b.getLength()<<std::endl;
std::cout<<a.getData()<<" "<<b.getData()<<std::endl;
a.swap(b);
std::cout<<a.getLength()<<" "<<b.getLength()<<std::endl;
std::cout<<a.getData()<<" "<<b.getData()<<std::endl;
return 0;
}
运行结果为:
11 22
0x1a31010 0x1a31050
22 11
0x1a31050 0x1a31010