移动构造和移动赋值
前面已经讨论过通过重载拷贝构造函数和赋值运算符来实现 move 导致的一系列问题。现在我们来看看 C++ 11 如何通过移动构造和移动赋值来解决这些问题。
1. 拷贝构造函数和拷贝赋值
首先来回顾一下 copy 语义。拷贝构造函数通过拷贝已经存在的对象来初始化一个新的对象。copy 赋值用于将一个对象拷贝给另外一个对象。如果没有定义,c++编译器会给一个默认的拷贝构造函数和拷贝赋值函数,这两个函数仅做浅拷贝,所以对于含有动态申请内存的类来说,这两个默认函数存在一些问题,因此,处理带有动态内存的类必须要重写这两个函数做深拷贝。 在之前的文章中,我们已经实现过两个版本的Auto_ptr,现在来看看第三个版本
template<class T>
class Auto_ptr3
{
T* m_ptr;
public:
Auto_ptr3(T* ptr = nullptr)
:m_ptr(ptr)
{
}
~Auto_ptr3()
{
delete m_ptr;
}
// Copy constructor
// Do deep copy of a.m_ptr to m_ptr
Auto_ptr3(const Auto_ptr3& a)
{
m_ptr = new T;
*m_ptr = *a.m_ptr;
}
// Copy assignment
// Do deep copy of a.m_ptr to m_ptr
Auto_ptr3& operator=(const Auto_ptr3& a)
{
// Self-assignment detection
if (&a == this)
return *this;
// Release any resource we're holding
delete m_ptr;
// Copy the resource
m_ptr = new T;
*m_ptr = *a.m_ptr;
return *this;
}
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
bool isNull() const { return m_ptr == nullptr; }
};
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
Auto_ptr3<Resource> generateResource()
{
Auto_ptr3<Resource> res(new Resource);
return res; // this return value will invoke the copy constructor
}
int main()
{
Auto_ptr3<Resource> mainres;
mainres = generateResource(); // this assignment will invoke the copy assignment
return 0;
}
在这个例子中,我们在main函数中调用generateResource函数得到一个智能指针,接着赋值给一个已经存在的智能指针mainres。这个程序的运行结果是:
Resource acquired
Resource acquired
Resource destroyed
Resource acquired
Resource destroyed
Resource destroyed
(如果编译器做了返回值优化(RVO)的话,也可能仅有四条输出)。 短短两行代码,却有这么多次资源创建和销毁,是怎么回事?我们来具体看一下到底发生了什么!
- 在generateResource函数中,局部变量res被创建出来,并用动态申请的Resource来初始化,因此打印出第一行Resource acquired。
- 局部变量res通过传值返回给main函数,这里通过调用拷贝构造函数将res拷贝到一个临时对象,因为我们的拷贝是深拷贝,因此会再次创建一个Resource,所以会打印出第二次 Resource acquired。
- generateResource函数返回,此时res出作用域被销毁,因此打印出第一个Resource destroyed。
- 刚才产生的临时对象被拷贝到mainres,这是通过拷贝赋值函数实现的,拷贝赋值也是深拷贝,因此会再次创建一个Resource,所以会打印出第三个Resource acquired。
- 拷贝赋值完成后,临时对象出作用域被销毁,因此打印出第二个Resource destroyed。
- main函数结束后,mailres出作用域被销毁,因此打印出第三个Resource destroyed。
可以看出来,因为我们调用了一次拷贝构造函数和一次拷贝赋值函数,因此多出来两次不必要的Resource构造和销毁。效率很低,但是至少没有crash。现在,有了c++11的move semantics,我们可以做的更高效。
2. 移动构造函数和移动赋值
C++11定义了两个新的函数以实现move semantics,分别是移动构造函数和移动赋值函数。不同于拷贝构造函数和拷贝赋值将一个对象拷贝到另外一个对象,移动构造函数和移动赋值函数将对象的ownership从一个对象移动到另一个对象,显然,移动的开销要比拷贝的开销小很多。 再来看拥有移动构造函数和移动赋值函数的auto_ptr版本。
#include <iostream>
template<class T>
class Auto_ptr4
{
T* m_ptr;
public:
Auto_ptr4(T* ptr = nullptr)
:m_ptr(ptr)
{
}
~Auto_ptr4()
{
delete m_ptr;
}
// Copy constructor
// Do deep copy of a.m_ptr to m_ptr
Auto_ptr4(const Auto_ptr4& a)
{
m_ptr = new T;
*m_ptr = *a.m_ptr;
}
// Move constructor
// Transfer ownership of a.m_ptr to m_ptr
Auto_ptr4(Auto_ptr4&& a) noexcept
: m_ptr(a.m_ptr)
{
a.m_ptr = nullptr; // we'll talk more about this line below
}
// Copy assignment
// Do deep copy of a.m_ptr to m_ptr
Auto_ptr4& operator=(const Auto_ptr4& a)
{
// Self-assignment detection
if (&a == this)
return *this;
// Release any resource we're holding
delete m_ptr;
// Copy the resource
m_ptr = new T;
*m_ptr = *a.m_ptr;
return *this;
}
// Move assignment
// Transfer ownership of a.m_ptr to m_ptr
Auto_ptr4& operator=(Auto_ptr4&& a) noexcept
{
// Self-assignment detection
if (&a == this)
return *this;
// Release any resource we're holding
delete m_ptr;
// Transfer ownership of a.m_ptr to m_ptr
m_ptr = a.m_ptr;
a.m_ptr = nullptr; // we'll talk more about this line below
return *this;
}
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
bool isNull() const { return m_ptr == nullptr; }
};
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
Auto_ptr4<Resource> generateResource()
{
Auto_ptr4<Resource> res(new Resource);
return res; // this return value will invoke the move constructor
}
int main()
{
Auto_ptr4<Resource> mainres;
mainres = generateResource(); // this assignment will invoke the move assignment
return 0;
}
移动构造和移动赋值是很简单的,我们这是简单的把新对象的指针指向了原有对象的资源,然后将原有对象的指针置为空, 这样就把资源从原有对象“移动”到了新对象。这段代码的执行结果是:
Resource acquired
Resource destoryed
看,这样好多了。这段代码的执行流程跟上面那个例子一样,只是在调用拷贝构造函数和拷贝赋值函数的时候,分别调用了移动构造函数和移动赋值函数。
- 在generateResource函数中,局部变量res被创建出来,并用动态申请的Resource来初始化,因此打印出第一行Resource acquired。
- 局部变量res通过传值返回给main函数,这里通过调用移动构造函数将res移动到一个临时对象。
- generateResource函数返回,此时res出作用域被销毁,因为此时res是一个空指针,因此什么也没有发生。
- 刚才产生的临时对象被移动到mainres,这是通过移动赋值函数实现的。
- 移动赋值完成后,临时对象出作用域被销毁,此时临时对象的指针为空,因此什么也没发生。
- main函数结束后,mailres出作用域被销毁,因此打印出第三个Resource destroyed。
这段代码仅有一次资源的构造和析构,但是有两次move。
3. 什么时候会调用移动构造和移动赋值呢?
如果一个类定义类移动构造函数和移动赋值函数,并且传参传的是右值的时候,会调用移动构造或移动赋值函数。一般情况下,这个右值要么是字面量,要么是临时对象。 在大多数情况下,编译器不会提供默认的移动构造和移动赋值函数,除非这个类没有定义拷贝构造函数,拷贝赋值函数,移动构造函数,移动赋值函数和析构函数。 而且,编译器提供的移动构造和移动赋值函数跟默认的拷贝构造,拷贝赋值函数做的事情一样。
Rule: 如果需要移动构造和移动赋值,你必须自己写。
4. move semantics 的核心思想
如果我们在构造或者赋值时,传入的参数是一个左值,我们只能做copy,不能move,因为左值在后面还有可能会用到,我们不能认为修改左值是安全的。比如a=b这行代码,我们不能指望b能被改变。
如果调用构造或者赋值时,传入的参数是右值,我们知道右值只是某种临时对象,我们可以放心的move而不是copy。这样做是安全的,因为右值在语句结束就会被销毁,我们不可能在接下里的代码中继续使用它。
C++11通过右值引用给我们提供了根据不同参数(左值还是右值)调用不同构造或赋值函数的能力,让代码更高效。
5. move 函数应该让两边的对象都处于定义良好的状态
在上面的例子中,移动构造和移动赋值函数最后都将原有对象的指针置为nullptr了,这看起来是不必要的,毕竟如果传入的对象是一个右值,那么在语句结束的时候总是要被销毁的,为什么我们要多此一举在函数里面做清理工作呢?
答案很简单,当传入的右值出了作用域的时候,它的析构函数会被调用,如果它的指针还指向原来申请的内存,那么这块内存会被释放,导致新对象的指针成为野指针。
另外,前面说到传入的参数是右值时,会调用移动构造或移动赋值函数,其实,传入左值也可以选择调用移动构造或移动赋值函数,这个在后面会继续讨论。
6. 函数按值返回的左值可以被 move 而不必 copy
在auto_ptr4版本的generateResource函数,res是通过传值返回的,虽然它是一个左值,但是它调用的是move而不是copy。C++规范中有一个特殊的规定,从函数返回的automatic object如果是通过传值返回的话,可以被move而不需要copy,即便他们是一个左值。这个是合理的,既然generateResource函数中的res在函数结束的时候马上就要被销毁掉,那么我们“偷”一下它的资源也是合理的,这样就避免了昂贵又没有必要的copy开销。
尽管编译器可以move函数返回值,但是在某些case,还可以更进一步,在这些case,不管是move构造函数还是copy构造函数都不会被调用(即返回值优化RVO)。
7. 禁止拷贝
在上面的auto_ptr4版本中,我们保留了拷贝构造函数和拷贝赋值函数是为了和移动构造函数,移动赋值函数做比较。但是有时候我们需要禁止copy,因为copy的开销很大,而且有时候对象T也不支持copy。 下面这个版本的Auto_ptr支持move但是不支持copy。
#include <iostream>
template<class T>
class Auto_ptr5
{
T* m_ptr;
public:
Auto_ptr5(T* ptr = nullptr)
:m_ptr(ptr)
{
}
~Auto_ptr5()
{
delete m_ptr;
}
// Copy constructor -- no copying allowed!
Auto_ptr5(const Auto_ptr5& a) = delete;
// Move constructor
// Transfer ownership of a.m_ptr to m_ptr
Auto_ptr5(Auto_ptr5&& a) noexcept
: m_ptr(a.m_ptr)
{
a.m_ptr = nullptr;
}
// Copy assignment -- no copying allowed!
Auto_ptr5& operator=(const Auto_ptr5& a) = delete;
// Move assignment
// Transfer ownership of a.m_ptr to m_ptr
Auto_ptr5& operator=(Auto_ptr5&& a) noexcept
{
// Self-assignment detection
if (&a == this)
return *this;
// Release any resource we're holding
delete m_ptr;
// Transfer ownership of a.m_ptr to m_ptr
m_ptr = a.m_ptr;
a.m_ptr = nullptr;
return *this;
}
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
bool isNull() const { return m_ptr == nullptr; }
};
在这个版本中,如果你尝试将一个Auto_ptr5左值通过值传递给一个函数,编译器会报错拷贝构造函数被删除了。这是很好的,因为我们无论如何都应该通过const左值引用来传递Auto_ptr。
Auto_ptr5 是一个比较不错的智能指针,实际上标准库中的std::unique_ptr跟auto_ptr5非常类似,推荐使用。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论