对于含有堆内存的类,我们需要提供深拷贝的拷贝构造函数,如果使用默认构造函数,会导致堆内存的重复删除,比如下面的代码:
class A { public: A() :m_ptr(new int(0)) { } ~A() { delete m_ptr; } private: int* m_ptr; }; // 为了避免返回值优化,此函数故意这样写 A Get(bool flag) { A a; A b; if (flag) return a; else return b; } int main() { A a = Get(false); // 运行报错 }
在上面的代码中,默认构造函数是浅拷贝,a和b会指向同一个指针m_ptr,在析构的时候会导致重复删除该指针。正确的做法是提供深拷贝的拷贝构造函数,比如下面的代码(关闭返回值优化的情况下):
class A { public: A() :m_ptr(new int(0)) { cout << "construct" << endl; } A(const A& a):m_ptr(new int(*a.m_ptr)) // 深拷贝 { cout << "copy construct" << endl; } ~A() { cout << "destruct" << endl; delete m_ptr; } private: int* m_ptr; }; int main() { A a = Get(false); // 运行正确 } 上面的代码将输出: construct construct copy construct destruct destruct destruct
这样就可以保证拷贝构造时的安全性,但有时这种拷贝构造却是不必要的,比如上面代码中的拷贝构造就是不必要的。上面代码中的Get函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对象b,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么,这个拷贝构造的代价会很大,带来了额外的性能损耗。有没有办法避免临时对象的拷贝构造呢?答案是肯定的。看下面的代码:
class A { public: A() :m_ptr(new int(0)) { cout << "construct" << endl; } A(const A& a):m_ptr(new int(*a.m_ptr)) // 深拷贝 { cout << "copy construct" << endl; } A(A&& a) :m_ptr(a.m_ptr) { a.m_ptr = nullptr; cout << "move construct: " << endl; } ~A() { cout << "destruct" << endl; delete m_ptr; } private: int* m_ptr; }; int main() { A a = Get(false); // 运行正确 } 上面的代码将输出: construct construct move construct destruct destruct destruct
上面的代码中没有了拷贝构造,取而代之的是移动构造(Move Construct)。从移动构造函数的实现中可以看到,它的参数是一个右值引用类型的参数A&&,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。这里的A&&用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高性能。这也就是所谓的移动语义(move语义),右值引用的一个重要目的是用来支持移动语义的。
移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高C++应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响。
以代码清单2-2所示为示例,实现拷贝构造函数和拷贝赋值操作符。
代码清单2-2 MyString类的实现
class MyString { private: char* m_data; size_t m_len; void copy_data(constchar *s) { m_data = newchar[m_len+1]; memcpy(_data, s, m_len); m_data[_len] = '\0'; } public: MyString(){ m_data = NULL; m_len = 0; } MyString(const char* p) { m_len = strlen (p); copy_data(p); } MyString(const MyString& str) { m_len = str.m_len; copy_data(str.m_data); std::cout <<"Copy Constructor is called! source: "<< str.m_data << std::endl; } MyString&operator=(const MyString& str) { if (this != &str) { m_len = str.m_len; copy_data(str._data); } std::cout <<"Copy Assignment is called! source: "<< str.m_data << std::endl; return *this; } virtual ~MyString() { if (m_data) free(m_data); } }; void test() { MyString a; a = MyString("Hello"); std::vector<MyString> vec; vec.push_back(MyString("World")); }
实现了调用拷贝构造函数的操作和拷贝赋值操作符的操作。MyString("Hello")和MyString("World")都是临时对象,也就是右值。虽然它们是临时的,但程序仍然调用了拷贝构造和拷贝赋值函数,造成了没有意义的资源申请和释放的操作。如果能够直接使用临时对象已经申请的资源,既能节省资源,又能节省资源申请和释放的时间。这正是定义移动语义的目的。
用C++11的右值引用来定义这两个函数,如代码清单2-3所示。
代码清单2-3 MyString的移动构造函数和移动赋值函数
MyString(MyString&& str) { std::cout <<"Move Constructor is called! source: "<< str._data << std::endl; _len = str._len; _data = str._data; // 避免了不必要的拷贝 str._len = 0; str._data = NULL; } MyString&operator=(MyString&& str) { std::cout <<"Move Assignment is called! source: "<< str._data << std::endl; if (this != &str) { _len = str._len; _data = str._data; // 避免了不必要的拷贝 str._len = 0; str._data = NULL; } return *this; } 再看一个简单的例子,代码如下: struct Element { Element(){} // 右值版本的拷贝构造函数 Element(Element&& other) : m_children(std::move(other.m_children)){} Element(const Element& other) : m_children(other.m_children){} private: vector<ptree> m_children; }; 这个Element类提供了一个右值版本的构造函数。这个右值版本的构造函数的一个典型应用场景如下: void Test() { Element t1 = Init(); vector<Element> v; v.push_back(t1); v.push_back(std::move(t1)); }
先构造了一个临时对象t1,这个对象中一个存放了很多Element对象,数量可能很多,如果直接将这个t1用push_back插入到vector中,没有右值版本的构造函数时,会引起大量的拷贝,这种拷贝会造成额外的严重的性能损耗。通过定义右值版本的构造函数以及std::move(t1)就可以避免这种额外的拷贝,从而大幅提高性能。
有了右值引用和移动语义,在设计和实现类时,对于需要动态申请大量资源的类,应该设计右值引用的拷贝构造函数和赋值函数,以提高应用程序的效率。需要注意的是,我们一般在提供右值引用的构造函数的同时,也会提供常量左值引用的拷贝构造函数,以保证移动不成还可以使用拷贝构造。
需要注意的一个细节是,我们提供移动构造函数的同时也会提供一个拷贝构造函数,以防止移动不成功的时候还能拷贝构造,使我们的代码更安全。