一. std::moveios
(一)std::move的原型编程
template<typename T> decltype(auto) move(T&& param) //注意,形参是个引用(万能引用) { using ReturnType = typename remove_reference<T>::type&&; //去除T自身可能携带的引用 return static_cast<ReturnType>(param); //强制转换为右值引用类型 }
(二)注意事项安全
1. std::move的本质就强制类型转换,它无条件地将实参转为右值引用类型(匿名对象,是个右值),继而用于移动语义。函数
2. 该函数只是将实参转为右值,除此以外并无真正的move任何东西。实际上,它在运行期没任何做为,编译器也不会为它生成任何的可执行代码,连一个字节都没有。性能
3. 若是要对某个对象执行移动操做时,则不要将其声明为常量。由于针对常量对象执行移动操做将变成复制操做。测试
二. 移动语义优化
(一)深拷贝和移动的区别this
1. 深拷贝:将SrcObj对象拷贝到DestObj对象,须要同时将Resourse资源也拷贝到DestObj对象去。这涉及到内存的拷贝。spa
2. 移动:经过“偷”内存的方式,将资源的全部权从一个对象转移到另外一个对象上。但只是转移,并无内存的拷贝。可见Resource的全部权只是从SrcObj对象转移到DestObj对象,因为不存在内存拷贝,其效率通常要高于复制构造。指针
(二)复制和移动操做函数
1. 复制/移动操做的函数声明
①Object(T&); //复制构造,仅接受左值 ②Object(const T&); //复制构造,便可以接受左值又可接收右值 ③Object(T&&) noexcept; //移动构造,仅接受右值 ④T& operator=(const T&);//复制赋值函数,便可以接受左值又可接收右值 ⑤T& operator=(T&&); //移动赋值函数,仅接受右值
2. 注意事项
①移动语义必定是要修改临时对象的值,因此声明移动构造时应该形如Test(Test&&),而不能声明为Test(const Test&&)
②默认的移动构造函数实际上跟默认的拷贝构造函数同样,都是“浅拷贝”。一般状况下,必须自定义移动构造函数。
③对于移动构造函数来讲,抛出异常是很危险的。由于移动语义还没完成,一个异常就抛出来,可能会形成悬挂指针。所以,应尽可能经过noexcept声明不抛出异常,而一旦出现异常就能够直接调用std::terminate终止程序。
④特殊成员函数之间存在相互抑制的生成机制,可能会影响到默认拷贝构造和默认移动构造函数的自动生成。(详见《特殊成员函数的生成机制》一节)
【编程实验】move移动语义
#include <iostream> #include <vector> using namespace std; //1. 移动语义 class HugeMem { public: int* buff; int size; HugeMem(int size) : size(size > 0 ? size : 1) { buff = new int[size]; } //移动构造函数 HugeMem(HugeMem&& hm) noexcept : size(hm.size), buff(hm.buff) { hm.buff = nullptr; } ~HugeMem() { delete[] buff; } }; class Moveable { public: HugeMem h; int* i; public: Moveable() : i(new int(3)), h(1024){} //移动构造函数(强制转为右值,以调用h的移动构造函数。注意m虽然是右值 //引用,但形参是具名变量,m是个左值。所以m.h也是左值,需转为右值。 Moveable(Moveable&& m) noexcept: i(m.i), h(std::move(m).h) { m.i = nullptr; } ~Moveable() { delete i; } }; Moveable GetTemp() { Moveable tmp = Moveable(); cout << hex << "Huge mem from " << __func__ << " @" << tmp.h.buff << endl; return tmp; } //2. 对常量对象实施移动将变成复制操做 class Annotation { std::string value; public: //注意:对常量的text对象实施移动操做时,因为std::move(text)返回的结果是个 //const std::string对象,因为带const,不能匹配string(&& rhs)移动构造函数, //但匹配string(const string& rhs)复制构造函数,所以当执行value(std::move(text)) //时,其实是将text复制给value。对于非string类型的状况也同样,所以对常量对象的 //移动操做实际上会变成复制操做! explicit Annotation(const std::string text) : value(std::move(text)) { } }; //3. 利用移动语义实现高性能的swap函数 template<typename T> void Swap(T& a, T& b) noexcept //声明为noexcept以便在交换失败时,终止程序 { //若是a、b是可移动的,则直接转移资源的全部权 //若是是不可移动的,则经过复制来交换两个对象。 T tmp(std::move(a)); //先把a的资源转交给tmp a = std::move(b); b = std::move(tmp); } int main() { //1. 移动语义 Moveable a(GetTemp()); //移动构造 cout << hex << "Huge mem from " << __func__ << " @" << a.h.buff << endl; return 0; } /*输出结果 Huge mem from GetTemp @02C66248 (从中能够看出Huge mem从临时对象移动了a对象) Huge mem from main @02C66248 */
3、正确理解移动语义
(一) “移动”操做其实是一种请求,由于有些类型不存在移动操做,对于这些对象会经过其复制操做来实现“移动”。
(二)某些类型的移动操做未必比复制操做更快。如:
1. std::vector和std::array。
(1)标准库大部分容器类(如vector),内部是将其元素Widgets存放在堆上,而后用指针指向该堆内存。在进行移动操做时,只是进行指针的复制。整个容器内容在常数时间内即可移动完成。
(2)而std::array对象缺乏这样的一根指针,由于其内容数据是直接存储对象上的。虽然std::array提供移动操做,但其移动和复制的速度哪一个更快,取决于元素Widget的移动和复制速度的比较。同时std::array移动时须要对每个元素进行移动,老是须要线性时间。
2. 许多std::string类型的实现采用了小型字符串优化(SSO)。当使用SSO后,“小型”字符串(如不超过15个字符)会存储在std::string对象内的某个缓冲区内,即内容直接存储在对象上(而不是堆上)。所以,此时是整个对象的移动,速度并比复制更快。
(三)标准库一些容器操做提供了强异常安全保证,为了兼容C++98的遗留代码在升级到C++11时仍保证正确性。库中用std::move_if_noexcept模板来替代move函数。该函数在类的移动构造函数没有声明noxcept关键字时返回一个左值引用从而使变量经过拷贝语义,而在移动构造函数有noexcept时返回一个右值引用,从而使变量可使用移动语义。移动操做未加noexcept时,编译器仍会强制调用一个复制操做。
【编程实验】正确理解移动语义
#include <iostream> #include <chrono> #include <vector> #include <array> #include <thread> using namespace std; //1. 移动不存在时,实行的是复制操做 class Foo { public: Foo(){} Foo(const Foo&) { cout <<"Foo(const Foo&)" << endl; } }; //2. 移动速度未必比复制快 //2.1 辅助类(元素类) class Widget { public: Widget() = default; Widget(const Widget&) { //模拟复制操做,假设须要1毫秒 std::this_thread::sleep_for(std::chrono::milliseconds(1)); } Widget(Widget&&) { //模拟移动操做,假设须要2毫秒 std::this_thread::sleep_for(std::chrono::milliseconds(2)); } Widget& operator=(const Widget&) { //模拟复制赋值操做,假设须要1毫秒 std::this_thread::sleep_for(std::chrono::milliseconds(1)); return *this; } Widget& operator=(Widget&&) { //模拟移动赋值操做,假设须要2毫秒 std::this_thread::sleep_for(std::chrono::milliseconds(2)); return *this; } }; //2.2. 计算任意函数的执行时间:auto&&用于lambda表达式形参(C++14) auto funcTimer = [](auto&& func, auto&& ... params) { //计时器启动 std::chrono::system_clock::time_point t1 = std::chrono::system_clock::now(); //调用func(param...)函数 std::forward<decltype(func)>(func)( //根据func的左右值特性来调用相应的重载&或&&版本的成员函数 std::forward<decltype(params)>(params)... //保持参数的左/右值特性 ); std::chrono::system_clock::time_point t2 = std::chrono::system_clock::now(); long long elapsed = std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1).count(); cout << elapsed << " microseconds" << endl; }; //2.3 复制和移动操做 auto lamMove = [](auto&& src) { auto dest = std::move(src); return; }; auto lamCopy = [](auto&& src) { auto dest = src; return; }; //2.4 测试vector类 void testVector() { std::vector<Widget> vw1{ 10,Widget() }; cout <<"copy vector: " ; funcTimer(lamCopy, vw1); //测试移动操做用时 cout << "move vector: "; funcTimer(lamMove, vw1); } //2.5 测试array类 void testArray() { std::array<Widget, 10> aw1; cout << "copy array: "; funcTimer(lamCopy, aw1); //测试移动操做用时 cout << "move array: "; funcTimer(lamMove, aw1); } //3. move_if_noexcept的用法 struct Maythrow { Maythrow() {} Maythrow(const Maythrow&) { cout <<"Maythrow copy construct." << endl; } Maythrow(Maythrow&&) { cout << "Maythrow move construct." << endl; } }; struct Nothrow { Nothrow() {} Nothrow(const Nothrow&) { cout << "Nothrow copy construct." << endl; } Nothrow(Nothrow&&) noexcept { //注意,这里声明为noexcept! cout << "Nothrow move construct." << endl; } }; int main() { //1. 移动操做不存在时 Foo f1; Foo f2 = std::move(f1); //调用复制构造函数 //2. 移动速度未必比复制快 testVector(); testArray(); //3. 移动未声明为noexcept时,调用复制构造 Maythrow m; Nothrow n; Maythrow mt = move_if_noexcept(m); //move_if_noexcept返回左值引用,调用复制构造函数 Nothrow nt = move_if_noexcept(n); //move_if_noexcept返回右值引用,调用移动构造函数 return 0; } /*输出结果 Foo(const Foo&) copy vector: 19825 microseconds move vector: 5 microseconds //常量时间 copy array: 19109 microseconds move array: 29589 microseconds //移动的速度未必比复制快!取决于Widget的移动和复制速度的比较! Maythrow copy construct. //调用复制构造函数 Nothrow move construct. //调用移动构造函数 */