1. 概述
本文将重点介绍"Move"语义相关的移动构造和移动赋值构造函数,同时也给出了与"Copy"语义的拷贝构造和拷贝赋值构造函数的对比。
2. 拷贝构造和拷贝赋值构造函数
在了解移动构造和移动赋值构造函数之前,我们先来看一下拷贝构造和拷贝赋值构造函数。
- 拷贝构造函数:通过拷贝的方式,用一个相同类的对象去初始化类对象;
- 拷贝赋值构造函数:通过拷贝的方式,用一个类对象赋值给一个相同类且已经存在的类对象。
如果代码中没有显式地给出拷贝构造和拷贝赋值构造函数,并且又会用到时,此时编译器将会生成一套默认的拷贝构造和拷贝赋值构造函数(“浅拷贝”版本)。如果类中又有处理动态分配内存时,我们应该重写自己的“深拷贝”版本。
回忆一下我们之前的文章《Move语义和Smart Pointers先导(以一个例子说明)》中第五章的方案二,当时说采用拷贝构造和拷贝赋值构造函数有代价,我们在这里看一下有什么代价。
#include <iostream>
template<typename T>
struct AutoPtr3
{
AutoPtr3(T* ptr = nullptr)
: ptr(ptr)
{
}
~AutoPtr3()
{
if(this->ptr != nullptr)
{
delete this->ptr;
this->ptr = nullptr;
}
}
AutoPtr3(const AutoPtr3& ptr3) // deep copy
{
this->ptr = new T;
*this->ptr = *ptr3.ptr;
}
AutoPtr3& operator=(const AutoPtr3& ptr3) // deep copy
{
if(this == &ptr3)
{
return *this;
}
delete this->ptr;
this->ptr = new T;
*this->ptr = *ptr3.ptr;
return *this;
}
T& operator*() const
{
return *this->ptr;
}
T* operator->() const
{
return this->ptr;
}
bool isNull() const
{
return this->ptr == nullptr;
}
private:
T* ptr;
};
struct Resource
{
Resource()
{
std::cout << "Resource acquired" << std::endl;
}
~Resource()
{
std::cout << "Resource destroy" << std::endl;
}
};
int main()
{
AutoPtr3<Resource> res1{new Resource()}; // local variable, constructor
AutoPtr3<Resource> res2;
res2 = res1; // copy assignment
return 0;
}
程序的输出如下:
Resource acquired
Resource acquired
Resource destroy
Resource destroy
此时的程序是按照下面的步骤运行的:
- 第一次Resource acquired发生在局部变量构造:AutoPtr3<Resource> res1{new Resource()};
- 第二次Resource acquired发生在拷贝赋值构造:res2 = res1;
- 第一次Resource destroy发生在拷贝赋值构造后main()即将结束时,先析构res2;
- 第二次Resource destroy发生在拷贝赋值构造后main()即将结束时,析构res1。
从上面可以看出,采用拷贝语义最起码有一次局部变量的构造+第二个变量的拷贝构造/拷贝赋值构造。
3. 移动构造和移动赋值构造函数
拷贝构造和拷贝赋值构造函数将一个对象拷贝给另外一个同类型的对象,而移动构造和移动赋值构造函数只是将一个对象的所有权转移到同类型的另一个对象上,显然"Move"语义比"Copy"语义代价小。
此处需要注意"Copy"语义的函数采用const左值引用,而"Move"语义的函数则采用non-const右值引用(左值引用和右值引用将在下一篇文章《左值、右值、左值引用和右值引用浅析》中介绍),代码如下:
#include <iostream>
template<typename T>
struct AutoPtr4
{
AutoPtr4(T* ptr = nullptr)
: ptr(ptr)
{
}
~AutoPtr4()
{
if(this->ptr != nullptr)
{
delete this->ptr;
this->ptr = nullptr;
}
}
AutoPtr4(const AutoPtr4& ptr4) = delete; // disable copying
AutoPtr4(AutoPtr4&& ptr4) noexcept // move constructor
: ptr(ptr4)
{
ptr4.ptr = nullptr;
}
AutoPtr4& operator=(const AutoPtr4& ptr4) = delete; // disable copy assignment
AutoPtr4& operator=(AutoPtr4&& ptr4) noexcept // move assignment
{
if(this == &ptr4)
{
return *this;
}
delete this->ptr;
this->ptr = ptr4.ptr;
ptr4.ptr = nullptr;
return *this;
}
T& operator*() const
{
return *this->ptr;
}
T* operator->() const
{
return this->ptr;
}
bool isNull() const
{
return this->ptr == nullptr;
}
private:
T* ptr;
};
struct Resource
{
Resource()
{
std::cout << "Resource acquired" << std::endl;
}
~Resource()
{
std::cout << "Resource destroy" << std::endl;
}
};
int main()
{
AutoPtr4<Resource> res;
res = new Resource(); // local variable construct + move assignment
return 0;
}
程序的输出如下:
Resource acquired
Resource destroy
此时的程序是这样运行的:
- Resource acquired是发生在临时的右值new Resource()构造;
- move assignment发生在res = new Resource();
- 移动赋值后临时的new Resource()生命周期结束,只是临时对象的销毁;
- Resource destroy发生在main()结束时,res被析构掉。
从中可以对比"Copy"语义和"Move"语义的效率差别,也就理解了为什么C++ 11要有"Move"语义。我们实现的这个类和标准库中的std::unique_ptr特别相似,我们将在《C++内存管理——Smart Pointers》中详细介绍。
4. 移动构造和移动赋值构造函数的调用
当且仅当上述的函数被定义且构造/赋值的参数是右值时,移动构造和移动赋值构造函数才会被调用。
另外,当不显式地定义移动构造和移动赋值构造函数时会生成默认的,而这套默认的"Move"语义的函数和默认的"Copy"语义的函数作相同的事情("Copy"并且是“浅拷贝”)。所以务必记住,若想使用"Move"语义的移动构造和移动赋值构造函数,必须自定义。
5. 总结
本文对比分析了拷贝构造、拷贝赋值构造函数和移动构造、移动赋值构造函数,可以看到"Move"语义相比于"Copy"语义在效率上的优越性,但是同时也要根据场景合理地进行使用。
欢迎大家批评指正、评论和转载(请注明源出处),谢谢!