2 C++移动语义
C++移动语义
参考文献
1 可拷贝和可移动的概念
- 在面向对象中,有的类是可以拷贝的,例如车、房等他们的属性是可以复制的,可以调用拷贝构造函数和拷贝赋值函数。
- 有点类的对象则是独一无二的,或者类的资源是独一无二的,比如 IO 、 std::unique_ptr等,他们不可以复制,但是可以把资源交出所有权给新的对象,称为可以移动的,可以调用移动构造函数和移动赋值函数。
- C++11最重要的一个改进之一就是引入了move语义,这样在一些对象的构造时可以获取到已有的资源(如内存)而不需要通过拷贝,申请新的内存,这样移动而非拷贝将会大幅度提升性能。例如有些右值即将消亡析构,这个时候我们用移动构造函数可以接管他们的资源。
2 移动构造函数和移动赋值函数
1 |
|
对于拷贝构造函数运行时可以看到
1 | class A construct! |
发生了一次构造和两次拷贝!在每次拷贝中数组都得重新申请内存,而被拷贝后的对象很快就会析构,这无疑是一种浪费。
对于移动移动构造函数;可以看到输出为
1 | class A construct! |
原先的两次构造变成了两次移动!!在移动构造函数中,我们做了什么呢,我们只是获取了被移动对象的资源(这里是内存)的所有权,同时把被移动对象的成员指针置为空(以避免移动过来的内存被析构),这个过程中没有新内存的申请和分配,在大量对象的系统中,移动构造相对与拷贝构造可以显著提高性能!这里noexcept告诉编译器这里不会抛出异常,从而让编译器省一些操作(这个也是保证了STL容器在重新分配内存的时候(知道是noexpect)而使用移动构造而不是拷贝构造函数),通常移动构造都不会抛出异常的。
移动构造和移动赋值
- 偷梁换柱直接“浅拷贝”右值引用的对象的成员;
- 需要把原先右值引用的指针成员置为 nullptr,以避免右值在析构的时候把我们浅拷贝的资源给释放了;
- 移动构造函数需要先检查一下是否是自赋值,然后才能先delete自己的成员内存再浅拷贝右值的成员,始终记住第2条。
3 std::move()
- std::move(lvalue) 的作用就是把一个左值转换为右值。关于左右值的含义我们上一篇博客C++11的右值引用进行过阐述。
1 | int lv = 4; |
如果使用std::move 函数
1 | int &&rr = std::move(lv); // 正确,把左值转换为右值 |
可以看到 std::move的作用是把左值转换为右值的。
让我们看一看 std::move 的源码实现:
1 | // FUNCTION TEMPLATE move |
可以看到std::move 是一个模板函数,通过remove_reference_t获得模板参数的原本类型,然后把值转换为该类型的右值。用C++大师 Scott Meyers 的在《Effective Modern C++》中的话说, std::move 是个cast ,not a move.
值得注意的是: 使用move意味着,把一个左值转换为右值,原先的值不应该继续再使用(承诺即将废弃)
4 使用 std::move 实现一个高效的 swap 函数
我们可以使用 move语义实现一个 交换操作,swap;在不使用 Move 语义的情况下
1 | swap(A &a1, A &a2){ |
如果使用 Move语义,即加上移动构造函数和移动赋值函数:
1 | void swap_A(A &a1, A &a2){ |
可以看到move语义确实可以提高性能,事实上, move语义广泛地用于标准库的容器中。C++11标准库里的std::swap 也是基于移动语义实现的。
说到了 swap, 那就不得不说一下啊 move-and-swap 技术了
5 Move and swap 技巧
看下面一段代码,实现了一个 unique_ptr ,和标准的std::unqiue_ptr的含义一致,智能指针的一种。
1 | template<typename T> |
在这里如果要按照常规办法写移动赋值函数,函数体内需要写一堆检查自赋值等冗长的代码。使用 move-and-swap语义,只用简短的两行就可以写出来。 在移动赋值函数中 source 是个局部对象,这样在形参传递过来的时候必须要调用拷贝构造函数(这里没有实现则不可调用)或者移动构造函数
,(事实上仅限右值可以传进来了)。然后 std::swap 负责把原先的资源和source 进行交换,完成了移动赋值。这样写节省了很多代码,很优雅。
1 移动语义的应用
右值引用的出现弥补了 C++ 在移动语义上的缺失。在右值引用出现之前,我们在函数调用传参时,只有两种语义:给它一份拷贝(按值传递),或者给它一份引用(按引用传递)。void inc_by_value(int i) { ++i; }
1 | void inc_by_ref(int &i) { ++i; } |
在上面的这个场景中,语义的缺失并不明显,但当我们处理持有资源的对象时,就不是那么和谐了:
1 | class Socket { |
- 成员函数take的作用是接管外部传入的套接字,当我们使用拷贝语义时,会使得两个 Socket 对象同时持有同一份资源,可能导致资源的重用;而当使用引用的语义时,我们修改了原对象使其不可用,但并没有将这一点明确告知原对象的使用者,这可能导致资源的误用。
- 在语法上支持移动语义,除了明确告知调用者语义之外,对自动化排错也是有积极意义的,编译器或者其他代码检查工具,可能可以通过语义分析排查亡值对象的错用。
- “return value optimization” 和“copy elision”。所以,我认为代码中使用右值引用最重要的目的还是其语义,与优化没多大关系。原来就会优化这个东西










