在有资源的类中,使用swap函数交换两个对象时,如果没有正确的使用swap函数,则会造成资源的拷贝和释放,导致性能降低。
环境
在运行测试代码时,使用了如下环境:
linux使用的是ubuntu 18,在ubuntu上使用的g++版本如下:
root@learner:~# g++ --version
g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
定义一个用于测试的类
class A
{
public:
A(const char *s = nullptr){
cout << "constuctor :" << this << endl;
if (s == nullptr){
str_ = new char[1];
*str_ = '\0';
}else{
int len = strlen(s);
str_ = new char[len + 1];
strcpy(str_, s);
}
}
A(const A &another){
cout << this << " cpy constructor from " << &another << endl;
int len = strlen(another.str_);
str_ = new char[len + 1];
strcpy(str_, another.str_);
}
A &operator=(const A &another){
cout << this << " operator= from " << &another << endl;
if (this != &another){
delete[] this->str_;
int len = strlen(another.str_);
this->str_ = new char[len + 1];
strcpy(this->str_, another.str_);
}
return *this;
}
A(A &&another){
cout << this << " move constructor from " << &another << endl;
this->str_ = another.str_;
another.str_ = nullptr;
}
A &operator=(A &&another){
cout << this << " move operator= from" << &another << endl;
if (this != &another){
delete[] this->str_;
this->str_ = another.str_;
another.str_ = nullptr;
}
return *this;
}
~A(){
cout << "deconstrutor :" << this << endl;
if (str_){
delete[] str_;
}
}
char * c_str(){
return str_;
}
private:
char *str_;
};
使用std::swap函数
使用标准库中的std::swap函数交换两个对象时,需要分为两种情况:一种是类内有移动构造和移动赋值运算符;另一类是没有移动构造和移动赋值运算符。
类内有移动构造和移动赋值运算符
int main()
{
A a("hello");
A b("world");
cout << "&a = " << &a << endl;
cout << "&b = " << &b << endl;
cout << "===========" << endl;
std::swap(a, b);
cout << "===========" << endl;
cout<<"a = "<<a.c_str()<<endl;
cout<<"b = "<<b.c_str()<<endl;
return 0;
}
constuctor :0x7ffd1dfe3758
constuctor :0x7ffd1dfe3760
&a = 0x7ffd1dfe3758
&b = 0x7ffd1dfe3760
===========
0x7ffd1dfe3720 move constructor from 0x7ffd1dfe3758
0x7ffd1dfe3758 move operator= from0x7ffd1dfe3760
0x7ffd1dfe3760 move operator= from0x7ffd1dfe3720
deconstrutor :0x7ffd1dfe3720
===========
a = world
b = hello
deconstrutor :0x7ffd1dfe3760
deconstrutor :0x7ffd1dfe3758
类提供了移动构造和移动赋值运算符时,交换两个对象发生了一次移动构造、两次移动赋值运算和一次析构。
调用std::swap函数之前的状态。
首先使用a对象,通过移动构造,构造出tmp对象。tmp对象构造结束后,a对象变成了将亡值,此时a对象内部的资源已被掏空。
通过移动赋值运算,将b对象内部的资源转移至对象a中,此时b对象变成了将亡值。
再次通过移动赋值运算,将tmp对象内部的资源转移值对象b中,tmp对象变成了将亡值。
当std::swap()函数接收时,tmp对象离开其作用域,调用析构器将其析构掉。
可以看出,在类提供了移动构造和移动赋值时,此时使用std::swap()交换对象时,仅仅是对象内部资源的搬移,并没有产生资源的拷贝,所以性能很高。
int main()
{
A a("hello");
A b("world");
cout << "&a = " << &a << endl;
cout << "&b = " << &b << endl;
cout << "===========" << endl;
//std::swap(a, b);
{
A tmp = std::move(a);
a = std::move(b);
b = std::move(tmp);
}
cout << "===========" << endl;
cout<<"a = "<<a.c_str()<<endl;
cout<<"b = "<<b.c_str()<<endl;
return 0;
}
constuctor :0x7ffc0df0daf0
constuctor :0x7ffc0df0daf8
&a = 0x7ffc0df0daf0
&b = 0x7ffc0df0daf8
===========
0x7ffc0df0db00 move constructor from 0x7ffc0df0daf0
0x7ffc0df0daf0 move operator= from0x7ffc0df0daf8
0x7ffc0df0daf8 move operator= from0x7ffc0df0db00
deconstrutor :0x7ffc0df0db00
===========
a = world
b = hello
deconstrutor :0x7ffc0df0daf8
deconstrutor :0x7ffc0df0daf0
通过这段代码测试可以看出,std::swap(a,b);
和如下代码的效果是一致的。
A tmp = std::move(a);
a = std::move(b);
b = std::move(tmp);
类内没有移动构造和移动赋值运算符
int main()
{
A a("hello");
A b("world");
cout << "&a = " << &a << endl;
cout << "&b = " << &b << endl;
cout << "===========" << endl;
std::swap(a, b);
cout << "===========" << endl;
cout<<"a = "<<a.c_str()<<endl;
cout<<"b = "<<b.c_str()<<endl;
return 0;
}
constuctor :0x7ffef856f578
constuctor :0x7ffef856f580
&a = 0x7ffef856f578
&b = 0x7ffef856f580
===========
0x7ffef856f540 cpy constructor from 0x7ffef856f578
0x7ffef856f578 operator= from 0x7ffef856f580
0x7ffef856f580 operator= from 0x7ffef856f540
deconstrutor :0x7ffef856f540
===========
a = world
b = hello
deconstrutor :0x7ffef856f580
deconstrutor :0x7ffef856f578
类没有提供了移动构造和移动赋值运算符时,交换两个对象发生了一次拷贝构造、两次拷贝赋值和一次析构。
调用std::swap函数之前的状态。
通过a对象拷贝构造出tmp对象,此时a对象内部的资源发生了一次拷贝。
将b对象赋值给a对象,先是将a对象内部的资源释放掉,之后拷贝一份b对象内部的资源。
将tmp对象赋值给b对象,此时也会发生资源的释放和拷贝。
std::swap函数调用结束后,tmp调用析构器将资源销毁。
此时std::swap(a,b)
等同于如下代码:
A tmp = a;
a = b;
b = tmp;
通过上面的分析可知,当类没有提供移动构造和移动赋值时,使用std::swap函数交换两个对象时,会导致多次资源的拷贝和释放。如果资源很大,会消耗大量性能。
提供自己的swap函数
在没有移动构造和移动赋值时,可以提供自己的swap函数,同样也可以避免资源的拷贝和释放。
class A
{
public:
...
void swap(A & another)
{
std::swap(str_,another.str_);
}
...
};
int main()
{
A a("hello");
A b("world");
cout << "&a = " << &a << endl;
cout << "&b = " << &b << endl;
cout << "===========" << endl;
a.swap(b);
cout << "===========" << endl;
cout<<"a = "<<a.c_str()<<endl;
cout<<"b = "<<b.c_str()<<endl;
return 0;
}
constuctor :0x7ffcf371c3c8
constuctor :0x7ffcf371c3d0
&a = 0x7ffcf371c3c8
&b = 0x7ffcf371c3d0
===========
===========
a = world
b = hello
deconstrutor :0x7ffcf371c3d0
deconstrutor :0x7ffcf371c3c8
在调用类内部成员函数的时候,直接将a对象和b对象内部资源的指针交换。在交换两个对象时,交换的仅仅是两个指针,效率很高。
class A
{
public:
...
void swap(A & another)
{
std::swap(str_,another.str_);
}
...
};
inline void swap(A & x, A &y)
{
x.swap(y);
}
提供一个全局的swap函数,和std::swap函数实现重载。当在调用swap的参数是A类对象时,会使用我们提供的全局函数,效率更高。
int main()
{
A a("hello");
A b("world");
cout << "&a = " << &a << endl;
cout << "&b = " << &b << endl;
cout << "===========" << endl;
swap(a,b);
cout << "===========" << endl;
cout<<"a = "<<a.c_str()<<endl;
cout<<"b = "<<b.c_str()<<endl;
return 0;
}
constuctor :0x7ffffd5c6c28
constuctor :0x7ffffd5c6c30
&a = 0x7ffffd5c6c28
&b = 0x7ffffd5c6c30
===========
===========
a = world
b = hello
deconstrutor :0x7ffffd5c6c30
deconstrutor :0x7ffffd5c6c28
在SGI版本的STL中,也是使用上面这种方式为容器提供交换函数。例如vector的交换函数源码如下:
//vector内部函数成员
void swap(vector<T, Alloc>& x) {
std::swap(start, x.start);
std::swap(finish, x.finish);
std::swap(end_of_storage, x.end_of_storage);
}
//全局函数
template <class T, class Alloc>
inline void swap(vector<T, Alloc>& x, vector<T, Alloc>& y) {
x.swap(y);
}
vector内部的swap函数,在交换两个vector容器时,仅仅交换了vector内部的三个指针。
总结
在有资源的类中,如果没有移动构造和移动赋值,此时调用标准库中的swap函数,会发生资源的拷贝和释放,降低程序的性能。我们可以为其提供一个全局重载的swap函数,在这个函数中,交换指向资源的指针就可以实现对象的交换,交换两个指针的效率比交换资源的效率高太多了。