引言
首先让我们考虑一个问题:把大象从冰箱里拿出来,一共分几步。
普通青年:三步。第一步,把冰箱门打开。第二步,把大象拿出来。第三步,把冰箱门关上。
Java青年:三步。第一步,把冰箱门打开。第二步,把大象的身份证拿出来。第三步,把冰箱门关上。
C++2B青年:两步。第一步,使用生命构造仪创造一个一模一样的大象。第二步,使用量子湮灭技术把冰箱里的大象杀掉。(借用知乎Tinro的说法) 1
目标问题
在C++的编写中,有时我们会使用一些函数来构造一个对象。为了不像上文的2B青年一样,我们只能这么写:
//定义构造对象函数
void initClass(SomeClass &tmp,const T &value){
tmp.value = value;
//doThings
}
//使用时要创建一个变量,在进行使用。
SomeClass local();
initClass(local,value);
或者我们也可以这么写:
SomeClass* initClass(const T &value){
SomeClass* p = new SomeClass();
p->value = value;
//doThings
return p;
}
//使用
SomeClass* p = initClass(value);
然而,第一种的写法实在是太丑陋了,第二种太危险,如果使用智能指针进行改进的话为什么不用java呢。如果可以使用以下的写法而且不影响性能,就完美了。
SomeClass initClass(T value){
SomeClass inner();
inner.value = value;
//doThings
return inner;
}
//使用
SomeClass outer = initClass(value);
本文就是要用最小的代价构造一种写法来实现第三种方法的函数编写。
性能分析
让我们分析一下第三种写法会出现什么性能。
假设编译器不采取任何优化。
- 构造内部对象:SomeClass inner;
- 拷贝构造临时对象:tmp = inner;
- 析构内部对象:~inner;//不要纠结于写法,是这个意思就好
- 拷贝构造outer对象:outer = tmp;
- 析构临时对象:~tmp
我们可以看到在返回的过程中存在一个临时对象,有一次构造和析构都是多余的。如果该对象内部存在大量内容,那么这里绝对是影响性能的关键点。
返回值优化
但是,幸好编译器是很聪明的,它帮我们做了很多事情。这里就有一个专门的优化方法:返回值优化(RVO-return value optimization23)
经过编译器优化(debug模式下就会进行优化了,已测试vs2015)后,我们的返回过程变成什么样了呢?
- 构造内部对象:SomeClass inner;
- 拷贝构造outer对象:outer = inner;//可以这样理解,但是编译器做的要比这复杂
- 析构内部对象:~inner;
这种情况下如果这个函数执行过程中创建了大批数据,并存放在inner对象中。那么在拷贝构造outer对象的时候会从inner复制一份相同的数据到outer对象中。此时,outer和inner都存放了一份相同数据。然后,析构内部对象inner,又把这些数据都释放掉了。这TM就很尴尬了。有没有办法可以不进行复制呢?答案是存在的,就是使用右值引用。
右值引用
右值引用是C++11的新特性。在编译器层面区分了左值和右值。
什么是右值
顾名思义,就是等号右面的值啊。(这TM还用你说)
在网上,右值有很多解释,例如没有名字的变量,返回的变量。而按我的理解就是右值换一个称呼就叫做临时变量。
int a = 1;//a是左值,1是右值
int b = 2;//b是左值,2是右值
int c = a;//c是左值,a是左值
a = a*b;//a,b是左值,a*b是右值
return a;// a是左值,返回的中间结果是右值
右值有什么用
右值没有用!(看到这里,你可能就骂了,没有用你说个蛋!!请稍安勿躁)
这里说的没有用是指在程序里,这个右值(临时变量)就不会再用了(临时变量嘛)。
但是请注意这个临时变量的内容是有用的,在丢弃这个临时变量之前把它的内容交换给别的变量,那么这个临时变量就可以去死了。右值就像是备胎,在情人节那天买了一大束花,要送给自己的女神,但是女神只拿走了他的花,却把他给踹走了。(然后送给自己的男神)
仔细分析一下这个过程,使用交换的方法可以省略掉复制内容的时间。加快程序。
总结一下,右值没有用,但是右值和左值交换内容是有意义的。C++区分了这两者就是希望加快这个多余复制的时间。
举个栗子:
class M {
public:
//左值拷贝构造函数
M(const M& m) :matrix(m.matrix) {}
//右值拷贝构造函数,交换内容
M(M&& m) :matrix() {
vector<vector<int>> *tmp,*_that = &(m.matrix),*_this = &(this->matrix);
*tmp = *_that;
*_that = *_this;
*_this = *tmp;
//简单写法:swap(m.metrix,this->metrix);
}
private:
vector<vector<int>> matrix;
};
template<class T>
void swap(T& a, T& b)
{
T tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
}
std::move()
函数就是强行将变量变成右值的过程。也就是强行变备胎(看作备胎)的过程,想想真可怕。
考虑如下的过程
M a;//含有一个1000*1000的矩阵
M b;//含有一个3*3的矩阵
swap(a,b);
在swap函数的执行过程中
T tmp(std::move(a));//tmp含有一个1000*1000的矩阵,a含有一个0*0的矩阵
a = std::move(b); //a含有一个3*3的矩阵,b含有一个0*0的矩阵
b = std::move(tmp); //b含有一个1000*1000的矩阵,tmp含有一个0*0的矩阵
总结一下,右值就是因为其无用而可以交换内容而正确性不受影响的性质。利用这个性质我们可以达到完美转发,重载operator=等功能4
构造返回对象的完美写法
基于右值引用我们就可以去除复制内容的时间开销。
class Thing {
public:
string content;
Thing() :content(""){
cout << "默认构造Thing-" << this << " with content: " << content << endl;
}
~Thing(){
cout << "析构Thing-" << this << " with content: " << content << endl;
}
Thing(const string &c) :content(c) {
cout << "左值构造Thing-" << this << " with content: " << content << endl;
}
Thing(string &&c) :content(c) {
cout << "右值构造Thing-" << this << " with content: " << content << endl;
}
Thing(const Thing &t) :content(t.content) {
cout << "左值拷贝构造Thing-" << this << " with content: " << content << endl;
}
//string 实现了自己的右值拷贝构造函数,于是直接调用即可。
//参数t接受一个右值作为参数,但传进来后有了名字导致其变成左值(t.content是左值)
Thing(Thing &&t) :content(move(t.content)) {
cout << "右值拷贝构造Thing-" << this << " with content: " << content << endl;
}
};
Thing test() {
cout << "-----------function-test begin---------" << endl;
string str = "large vector";
Thing t(str);
cout << "-----------function-test end-----------" << endl;
return t;
}
int main(int argc, char *argv) {
Thing local = test();
cout << "----------- copy complete -----------" << endl;
return 0;
}
测试
win10+vs2015,debug模式下:
-----------function-test begin---------
左值构造Thing-0097FC14 with content: large vector
-----------function-test end-----------
右值拷贝构造Thing-0097FD54 with content: large vector
析构Thing-0097FC14 with content:
----------- copy complete -----------
析构Thing-0097FD54 with content: large vector
请按任意键继续. . .
编译器优化了一个构造和析构的过程,使用右值引用优化了复制内容的时间开销。
-----------function-test begin---------
左值构造Thing-00CFF944 with content: large vector
-----------function-test end-----------
----------- copy complete -----------
析构Thing-00CFF944 with content: large vector
请按任意键继续. . .
使用release模式试一下,发现test函数被inline化了,导致中间结果的构造过程都被优化掉了。
后记
首先不得不感慨,C++太有(bu)意(shi)思(ren)了。
编译器优化真牛逼。
return stl容器,对性能几乎不会有影响。
感谢大家的阅读,欢迎进行留言讨论。