深拷贝的性能问题
如果一个类的构造函数申请了堆内存以存储数据,则必然需要在析构函数中释放堆内存,同时需要定义复制构造函数和重载赋值运算符,实现堆内数据的深拷贝。否则可能导致堆内存的二次释放。
但有时候这种深拷贝对于我们的程序来说是非必要的,而且如果存储在堆内的数据量非常大,深拷贝会引起很大的额外性能开销。此时,我们希望有一种方法来避免总是执行非必要的深拷贝。
C++11引入了右值引用和移动语义,来避免无意义的深拷贝,提高程序性能。
右值引用
左值是表达式结束后仍然存在的持久对象,右值是指表达式结束时就不存在的临时对象。
一个简单区分方法:如果可对表达式用&符取址,则为左值,否则为右值。
右值没有具体名字,只能通过引用来找到它。
使用&&来声明方法的参数为右值引用类型
先看下面代码:
class A
{
public:
A()
{
cout << "# a new A by native\n";
n = new int[5];
}
~A()
{
cout << "delete A\n";
delete [] n;
}
/* 复制构造函数,传入左值时 */
A(const A & a)
{
cout << "# a new A by copy\n";
n = new int[5];
memcpy(n, a.n, 5); // 深拷贝
}
/* 移动构造函数,传入右值时 */
A(A && a)
{
cout << "# a new A by move\n";
n = a.n; // 浅拷贝
a.n = nullptr;
}
private:
int *n;
};
// 这样子写是为了避免返回值被优化而导致无法出现右值引用
A getA(bool flag)
{
cout << "##### into getA now\n";
A a;
A b;
cout << "##### return now\n";
if(flag)
return a;
else
return b;
}
int main()
{
{
cout << "----- copy test\n";
A a1;
A a2(a1); // need copy
}
cout << "----- copy over\n";
{
cout << "----- move test\n";
A a3 = getA(true); // no copy, just move
}
cout << "----- move over\n";
}
输出为:
----- copy test
# a new A by native
# a new A by copy # 此处调用了复制构造函数,进行深拷贝
delete A
delete A
----- copy over
----- move test
##### into getA now
# a new A by native
# a new A by native
##### return now
# a new A by move # 函数的返回是一个临时变量,显然属于右值,因此对应地调用参数是右值引用的构造函数A(A && a)
delete A
delete A # 由于是引用,getA()函数返回的对象此时才被释放
delete A
----- move over
可见,利用右值引用定义的构造函数并不需要执行深拷贝,并且由于右值在外部并不会被引用到,所以在构造函数中将其资源直接转移出来是安全的。这就是所谓的移动语义(move),右值引用的一个重要目的是用来支持移动语义。对应的构造函数称为移动构造函数。一般来说还需要进一步重载采用右值引用的赋值运算符,称为移动赋值运算符。
移动语义的move方法
C++11提供了std::move()方法来将左值转换为右值,从而可以使得一般的左值也能够利用移动语义来优化性能。
move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。
示例:
int main()
{
A a1;
A a2 = move(a1);
}
输出:
# a new A by native
# a new A by move # 此时a1已不再拥有资源
delete A
delete A
forward 完美转发
当一个模板函数的参数类型定义为&&右值引用(void func(T && n)
),可以传入左值也可以传入右值。当一个右值引用传入该函数时,他在实参中有了命名n,那么此时如果继续向下调用其他函数(将n转发给其他函数),根据C++ 标准的定义,这个参数变成了一个左值。
如下所示,将1作为右值传入后,再将其传入 print,走的却是 print 的左值版本。
template<typename T>
void print(T && n)
{
cout << "R value " << n << endl;
}
template<typename T>
void print(T & n)
{
cout << "L value " << n << endl;
}
template<typename T>
void func(T && n)
{
print(n);
}
int main()
{
cout << "----------- func(1)\n";
func(1);
}
输出打印:
----------- func(1)
L value 1
如果继续向下转发的函数其左值版本存在深拷贝,则其对此优化的右值引用版本将不会被调用。此时可以引入完美转发std::forward
,恢复变量原来的引用属性。如果n传入前原来是左值,就恢复为左值,如果原来是右值就恢复为右值。
template<typename T>
void func(T && n)
{
print(n);
print(forward<T>(n)); // 完美转发-原来是右值,则转回右值
}
int main()
{
int n = 10;
cout << "----------- func(1)\n";
func(1);
cout << "----------- func(n)\n";
func(n); // n 是左值
}
----------- func(1)
L value 1 # print(n);
R value 1 # print(forward<int>(n));
----------- func(n)
L value 10
L value 10 # after forward , L is still L
emplace_back
对于STL容器,C++11后引入了emplace_back
接口,可用于替代push_back
,提高性能。
考虑下面的代码:
vector<string> vs;
vs.push_back("haha");
尽管 push_back 已经实现了右值引用的版本,但其调用过程中,传入字符串"haha"本身需要先构造一个string临时对象,然后vector内部又要调用一次移动构造函数,完了之后将传入的string临时对象析构。虽然比起传入左值引用而调用复制构造函数的方式要快,但构造和析构这个临时对象仍然显得多余。
c++11可以用 emplace_back 代替 push_back ,emplace_back 可以直接在 vector 中直接构建一个对象,而非创建一个临时对象再放进 vector
所以,以后应尽量使用emplace_back :
vector<string> vs;
vs.emplace_back("haha");