C++移动构造函数与移动赋值运算符

移动构造与移动赋值

拷贝构造函数与移动赋值的重载#

C++允许通过拷贝的方式来构造一个新的对象, 也允许通过移动赋值的方式构造一个新的对象.
在C++中新建一个类的时候, 默认的构造函数与移动赋值运算符都是基于浅拷贝执行的. 这个浅拷贝的构造函数与移动赋值运算符支持重载, 我们要说的就是重载这两个部分的不同方式.

拷贝构造与拷贝赋值运算符#

我们首先复习一下什么是拷贝构造与拷贝赋值, 拷贝构造通过复制一个类对象来构造相同类的另一个对象, 在我们新建一个类的时候, 该类有一个默认的浅拷贝构造函数, 浅拷贝会存在动态内存的问题, 因此我们通常会重载一个深拷贝的构造函数以及深拷贝的移动赋值函数. 这部分可以参考前面的博客, 传送门.
下面是拷贝构造函数的一个实例:

Copy
#include <iostream> template<typename 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; }

在上面的代码中, 在 return res; 会调用拷贝构造函数, 这个我们在后续解释 RVO 优化的时候会具体说一下, 目前不做解释, 这里只会在C++98版本调用拷贝构造函数.

移动构造函数与移动赋值运算符#

C++11 defines two new functions in service of move semantics: a move constructor, and a move assignment operator. Whereas the goal of the copy constructor and copy assignment is to make a copy of one object to another, the goal of the move constructor and move assignment is to move ownership of the resources from one object to another (which is typically much less expensive than making a copy).

C++11引入了新的移动构造函数与移动赋值操作符, 移动构造函数与移动赋值操作符避免了深拷贝带来的内存消耗, 将原来资源的权限赋值到一个新建的对象中.

Defining a move constructor and move assignment work analogously to their copy counterparts. However, whereas the copy flavors of these functions take a const l-value reference parameter (which will bind to just about anything), the move flavors of these functions use non-const rvalue reference parameters (which only bind to rvalues).

移动构造函数与移动复制操作符与拷贝操作的另一个不同是拷贝操作的参数是左值, 而移动构造函数与移动复制操作符的参数是右值, 关于左值引用与右值引用可以参考下面这篇文章, 我觉得描述很清晰 传送门.

Here’s the same Auto_ptr3 class as above, with a move constructor and move assignment operator added. We’ve left in the deep-copying copy constructor and copy assignment operator for comparison purposes.

Copy
#include <iostream> template<typename 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) { std::cout << "Call Deep Copy Constructor" << std::endl; 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) { std::cout << "Call Move Constructor" << std::endl; // 移动复制操作符中需要将右值引用的原指针清空, 防止外部修改权限转移过的内存 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) { std::cout << "Use Copy Assignment" << std::endl; // 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 { std::cout << "Use Move Assignment" << std::endl; // 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; }

上面的例子很好的展示了移动构造函数与移动复制运算符与拷贝构造函数, 拷贝复制运算符的不同, 虽然它们都需要重载默认的浅拷贝的运算符, 但是重载的实现方式还是不同的. 在移动复制操作符中有下列需要注意的地方:

  1. 移动赋值之前需要清空当前对象持有的资源.
  2. 移动赋值之后需要清空原对象持有的资源, 防止资源的篡改.
  3. 原对象与当前对象一致, 则不需要进行移动赋值操作.
  4. 移动构造函数与移动赋值操作符的参数都是右值引用, 通常使用 std::move() 将左值引用转换为右值引用.

我们来更深入的查看一下上述的函数是如何调用移动构造函数与移动赋值操作符的.

C++编译的RVO和NRVO优化机制#

上面的代码在 return res; 部分调用了移动构造函数是有一定限制的, 需要关闭 RVO 优化, 我们下面解释一下什么是 RVO 优化.
RVO 即 "Return Value Optimization", 是一种编译器优化技术, 通过该技术编译器可以减少函数返回时生成临时值(对象)的个数, 从某种程度上可以提高程序的运行效率, 对需要分配大量内存的类对象其值复制过程十分友好;
NRVO 全称为 “Named Return Value Optimization”, 该优化的大致流程与 RVO 类似;

只是单纯这么说显得比较空洞, 上面的代码就是使用 NRVO 的示例:
正常情况下, 我们使用编译命令 g++ rvo.cc -o rvo --std=c++11 && ./rvo 得到下面的输出:

Copy
Resource acquired Use Move Assignment Resource destroyed

可以看到仅在 mainres = generateResource();处使用了移动赋值操作符. 这个很好理解, 因为generateResource()作为函数返回值, 是一个右值, 这里使用移动赋值操作符. 但是没有调用移动构造函数, 这是因为编译器默认情况下是使用 NRVO 优化的.
当我们修改编译步骤后, 使用下面编译命令: g++ rvo.cc -o rvo --std=c++11 -fno-elide-constructors && ./rvo, 运行后的输出为:

Copy
Resource acquired Call Move Constructor Use Move Assignment Resource destroyed

此时我们可以看到调用了移动构造函数, 并且可以在x86的汇编代码中找到下面部分的代码:

Copy
lea rdx, [rbp-24] mov rax, QWORD PTR [rbp-40] mov rsi, rdx mov rdi, rax call Auto_ptr4<Resource>::Auto_ptr4(Auto_ptr4<Resource>&&) [complete object constructor] lea rax, [rbp-24] mov rdi, rax call Auto_ptr4<Resource>::~Auto_ptr4() [complete object destructor]

这部分代码显示的调用了移动构造函数, 与移动构造函数的析构函数. 在没有使用NRVO优化的时候, 移动构造函数的调用就十分明显.

RVO 优化的场景#

C++11 标准明确规定了在两个特定场景下, 编译器必须省略复制和移动构造, 即使这些构造函数有副作用. 这些场景分别是:

  1. 返回右值的 return 语句:
    当函数的返回值是与返回类型相同的纯右值(即一个临时对象)时, 编译器会将这个返回值直接构造在返回的位置上, 而不会调用复制或移动构造函数:
Copy
MyClass createObject() { return MyClass(); // 返回的是一个与返回类型相同的临时对象 }

在这个例子中, MyClass() 直接构造在返回的位置上, 避免了额外的复制或移动构造.
2. 初始化变量时, 使用与变量类型相同的纯右值:
当变量的初始化表达式为一个与变量类型相同的纯右值时, 编译器会将这个右值直接构造在目标变量的位置上.

Copy
MyClass obj = MyClass(); // 直接在 `obj` 的存储位置上构造对象

NRVO 优化的场景#

NRVO(Named Return Value Optimization):是指在有命名的返回值的情况下进行优化. 假设函数中返回了一个有名字的局部变量, 编译器可以在栈上直接构造该变量, 从而避免额外的复制或移动操作.例如:

Copy
MyClass createObject() { MyClass obj; return obj; // NRVO: 直接在调用处构造 `obj` }

这个例子就是我们前面所展示的例子, 返回值是一个局部变量.

移动构造与移动赋值运算符调用的场景#

The move constructor and move assignment are called when those functions have been defined, and the argument for construction or assignment is an rvalue. Most typically, this rvalue will be a literal or temporary value.

The copy constructor and copy assignment are used otherwise (when the argument is an lvalue, or when the argument is an rvalue and the move constructor or move assignment functions aren’t defined).

当传给构造函数与赋值运算符的是一个右值的时候就会调用移动构造函数与移动赋值运算符, 否则使用拷贝构造函数与拷贝赋值运算符

移动语义的关键思想#

You now have enough context to understand the key insight behind move semantics.

If we construct an object or do an assignment where the argument is an l-value, the only thing we can reasonably do is copy the l-value. We can’t assume it’s safe to alter the l-value, because it may be used again later in the program. If we have an expression “a = b” (where b is an lvalue), we wouldn’t reasonably expect b to be changed in any way.

However, if we construct an object or do an assignment where the argument is an r-value, then we know that r-value is just a temporary object of some kind. Instead of copying it (which can be expensive), we can simply transfer its resources (which is cheap) to the object we’re constructing or assigning. This is safe to do because the temporary will be destroyed at the end of the expression anyway, so we know it will never be used again!

C++11, through r-value references, gives us the ability to provide different behaviors when the argument is an r-value vs an l-value, enabling us to make smarter and more efficient decisions about how our objects should behave.

在 C++11 中, 移动语义提供了更加丰富以及多变的内存使用方式, 使用右值引用也能够极大的减小内存开销.

禁用COPY#

在某些场景中, 我们希望禁止使用拷贝构造函数与拷贝赋值运算符, 也许是某个对象的拷贝成本太大或者这个类不支持拷贝操作, 此时我们可以在类的实现中禁用拷贝语义, 禁用的方式如下:

Copy
#include <iostream> template<typename 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! // delete 关键字禁用拷贝构造函数 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! // delete 关键字禁用拷贝赋值运算符 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; } };

移动语义和 std::swap 的问题#

In lesson 21.12 -- Overloading the assignment operator, we mentioned the copy and swap idiom. Copy and swap also works for move semantics, meaning we can implement our move constructor and move assignment by swapping resources with the object that will be destroyed.

This has two benefits:

  1. The persistent object now controls the resources that were previously under ownership of the dying object (which was our primary goal).
  2. The dying object now controls the resources that were previously under ownership of the persistent object. When the dying object actually dies, it can do any kind of cleanup required on those resources.
    When you think about swapping, the first thing that comes to mind is usually std::swap(). However, implementing the move constructor and move assignment using std::swap() is problematic, as std::swap() calls both the move constructor and move assignment on move-capable objects. This will result in an infinite recursion issue.

这里是说明为什么在移动构造函数与移动赋值运算符中不可以使用 std::swaop() 函数, 因为 std::swap() 函数内部是调用了移动构造函数与移动赋值操作运算符的, 会导致递归的调用. std::swap() 函数内部使用移动语义避免内存的拷贝开销, 具体实现如下:

Copy
template <typename T> void swap(T& a, T& b) { T temp = std::move(a); // Step 1: Move a into temp (calls move constructor) a = std::move(b); // Step 2: Move b into a (calls move assignment) b = std::move(temp); // Step 3: Move temp into b (calls move assignment) }
posted @   虾野百鹤  阅读(136)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
点击右上角即可分享
微信分享提示
CONTENTS