尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C++11详解(二):右值引用、移动语义、std::move与完美转发

C++11详解(二):右值引用、移动语义、std::move与完美转发 C11详解二右值引用、移动语义、std::move与完美转发 星恒随风个人主页❄️ 个人专栏《指针合集》《C语言基础》《数据结构》《机器学习导论》《前端基础》《python基础》《C从入门到入土》《Linux的学习之旅》✨ 数据即知识压缩即智能前言为什么 C11 要引入右值引用1. 问题从一次“昂贵的拷贝”开始假设有strings1(hello world);string s2s1;由于string内部需要管理动态内存s1 ↓ 一块堆空间执行拷贝构造申请新空间 ↓ 复制字符串内容 ↓ 让 s2 管理新空间这是一种深拷贝如果对象非常大vector string list map 自定义资源管理类拷贝代价可能比较高。一、真正浪费的场景临时对象也要深拷贝吗例如stringGetString(){strings(very long string...);returns;}然后string retGetString();函数中的s马上就要结束生命周期。如果仍然申请一块新空间 ↓ 完整复制 s 的数据 ↓ 销毁 s就显得非常浪费。既然s 马上要死了为什么不能直接把 s 的资源交给 ret这就是移动语义 Move Semantics要解决的问题。二、先搞清左值和右值传统教学经常说等号左边的是左值 等号右边的是右值这个说法只能帮助入门并不严格。现代 C 更应该从表达式的值类别来理解。三、什么是左值常见左值inta10;int*pa;strings(hello);下面这些表达式通常都是左值a*p s s[0]它们最明显的特点之一是表示一个具有身份、可以持续存在的对象通常可以as取得地址。四、const 对象仍然是左值例如constintx10;虽然x20;不允许。但是x;完全合法。所以能不能修改不是区分左值和右值的核心。五、什么是右值常见例子10xystring(hello)GetString()其中很多属于临时结果这些结果通常没有必要长期保留原来的资源。因此它们非常适合移动六、左值引用我们以前写inta10;intra;C11 以后为了和右值引用区分可以称为左值引用语法T例如strings(hello);stringrs;七、右值引用C11 新增T称为右值引用例如intr110;stringr2string(hello);可以把它理解为给右值表达式产生的对象取一个别名。八、左值引用与右值引用的基本绑定规则普通左值引用intra;主要绑定左值。普通情况下不能// int r 10;1. const 左值引用比较特殊constintr10;是合法的。因此const T既能够绑定左值也可以绑定右值。这也是为什么 C98 中voidFunc(conststrings);已经能够非常灵活地接收参数。2. 右值引用intr10;可以绑定右值。但是inta10;// int r a;通常不允许。九、一个极其重要的规则右值引用变量本身是左值例如intrr10;10是右值。但变量rr是一个有名字的变量表达式所以后面使用rr时它是左值表达式。例如voidFunc(int);voidFunc(int);intrr10;Func(rr);匹配的是Func(int);而不是Func(int);十、为什么要这样设计因为intrr10;之后rr 已经有名字程序可能还要多次使用它。如果每次使用rr都自动把它当成“马上可以被掏空资源”的右值非常危险所以有名字的右值引用变量在表达式中仍然作为左值。如果程序员明确希望再次允许移动需要显式表达这个意图。这就引出了std::move十一、std::move 到底是什么首先必须纠正一个很常见的误解std::move 并不真正搬数据它主要做的是类型转换可以把典型实现简化理解成templateclassTremove_reference_tTmove(Tx){returnstatic_castremove_reference_tT(x);}它把对象转换成可以被当成右值处理的表达式更准确地说xvalue十二、move(a) 之后 a 马上没了吗没有。例如string ahello;string bstd::move(a);真正发生资源转移的是string 的移动构造函数而不是std::movestd::move(a)只是表达我允许接下来的操作把 a 当成可移动对象。十三、移动构造假设自己实现一个简化字符串classString{public:String(constchar*str){_strnewchar[strlen(str)1];strcpy(_str,str);}private:char*_strnullptr;};1. 拷贝构造String(constStrings){_strnewchar[strlen(s._str)1];strcpy(_str,s._str);}过程s ↓ 动态内存 A 申请动态内存 B ↓ 完整复制 A ↓ 新对象管理 B2. 移动构造String(Strings)noexcept:_str(s._str){s._strnullptr;}过程旧对象 ↓ 资源 A 直接把资源指针交给新对象 ↓ 旧对象清空指针没有重新申请 复制整块内容十四、为什么移动后一定要处理旧对象错误String(Strings):_str(s._str){}这样两个对象this s都指向同一块动态内存。析构时可能delete 两次所以移动以后必须保证源对象处于有效但资源已经被转移的状态。例如s._strnullptr;十五、移动并不意味着源对象不能析构恰恰相反。移动后的对象仍然必须可以安全析构 可以重新赋值通常称为valid but unspecified state也就是对象仍然有效 但具体保存什么值不应该再做假设。十六、移动赋值拷贝赋值Stringoperator(constStrings){// 深拷贝return*this;}移动赋值Stringoperator(Strings)noexcept{if(this!s){delete[]_str;_strs._str;s._strnullptr;}return*this;}核心思想仍然是不是复制资源 而是接管资源十七、移动构造和移动赋值什么时候真正有价值对于classPoint{intx;inty;};所谓“移动”与复制两个整数相比几乎没有区别移动语义真正有价值的类型通常是管理动态内存 管理文件句柄 管理 socket 管理锁 管理大型容器 拥有其他可移动成员也就是资源管理型对象十八、移动语义如何优化函数返回值例如vectorintCreateData(){vectorintv(1000000);// ...returnv;}没有移动语义时我们可能担心v ↓ 复制 100 万个元素 ↓ 返回对象C11 引入移动构造以后无法被更强的拷贝消除优化掉的情况下也可以v 的动态资源 ↓ 转交给返回对象而不必逐元素复制。十九、注意现代编译器还会进行拷贝消除实际执行vectorintretCreateData();并不意味着一定观察到一次移动构造因为编译器还可能进行RVO NRVO Copy Elision直接在最终对象位置构造。所以移动语义和拷贝消除不是同一个机制。二十、Linux 下如何观察构造行为可以自己定义带打印的构造 拷贝构造 移动构造 析构然后使用g test.cpp-stdc11-O0-fno-elide-constructors编译。其中-fno-elide-constructors可以帮助观察某些本来会被编译器消除的拷贝/移动过程。注意这是教学观察手段不代表实际工程中应该关闭优化。二十一、STL 为什么增加右值版本 push_backC11 以后voidpush_back(constvalue_typevalue);voidpush_back(value_typevalue);例如vectorstringv;string shello;v.push_back(s);s是左值走拷贝构造而v.push_back(string(hello));临时对象是右值可以走移动构造二十二、move 一个左值例如string shello;v.push_back(std::move(s));相当于明确告诉容器后面可以接管 s 的资源此后不要再假设shello但是s.clear();snew value;等重新赋值通常完全可以。二十三、引用延长临时对象生命周期例如conststringrstring(hello);临时string的生命周期会因为引用绑定而延长。右值引用也可以stringrstring(hello);并且r world;可以修改这个临时对象。二十四、值类别lvalue、prvalue、xvalueC11 对表达式值类别进一步细分。一个简化关系expression | ┌────────┴────────┐ ↓ ↓ glvalue rvalue / \ / \ / \ / \ lvalue xvalue xvalue prvalue1. lvalue例如inta;a2. prvalue典型10abstring(hello)可以粗略理解成纯右值3. xvalue例如std::move(a)或者static_castT(a)表示一个仍然具有身份 但资源允许被重新利用的对象所以将亡值这个中文名称非常形象。二十五、引用折叠C 源代码中不能直接intr;intr;但是在模板推导 typedef / using中可能间接形成“引用的引用”。C11 定义了引用折叠规则。1. 四种组合可以记成 → → → → 一句话只要出现左值引用结果就是只有两个都是才得到。二十六、为什么 template void Func(T) 不一定是右值引用例如templateclassTvoidFunc(Tx){}调用Func(10);10是右值T 推导为 int最终Func(int);1. 传左值inta10;Func(a);这里T 推导为 int于是T ↓ int ↓ 引用折叠 ↓ int最终Func(int);所以这种特定场景下的T不是简单的“只接右值”。通常称为转发引用 forwarding reference一些旧资料也称万能引用 universal reference二十七、问题又来了参数 x 在函数内部仍然是左值例如voidFun(int){coutlvalue\n;}voidFun(int){coutrvalue\n;}templateclassTvoidWrapper(Tx){Fun(x);}调用Wrapper(10);虽然10 是右值但是进入Wrapper后x 是一个有名字的变量所以Fun(x);最终调用Fun(int);右值属性丢失了。二十八、为什么不能直接 std::move(x)可以写Fun(std::move(x));这样右值确实能保持为右值。但问题是inta10;Wrapper(a);原始参数a 是左值你却无条件std::move(x)把它变成了右值。这样左值属性也被破坏了。我们真正需要的是原来是左值 → 继续作为左值 原来是右值 → 继续作为右值这就是完美转发 Perfect Forwarding二十九、std::forward典型用法templateclassTvoidWrapper(Tx){Fun(std::forwardT(x));}如果原始实参是左值T intforwardT(x)保持左值。如果原始实参是右值T intforwardT(x)恢复成右值。三十、move 与 forward 的区别这是必须掌握的区别。std::move语义无条件把对象视为可以移动的 xvalue典型std::move(obj)std::forward语义根据模板推导结果 有条件地保持原来的值类别典型std::forwardT(obj)主要出现在转发引用 可变参数模板 泛型包装函数 emplace中。三十一、完美转发最终解决了什么假设调用者 ↓ Wrapper ↓ 真正的 Fun我们希望调用者传左值 ↓↓ Fun 仍收到左值以及调用者传右值 ↓↓ Fun 仍收到右值最终templateclassTvoidWrapper(Tx){Fun(std::forwardT(x));}把类型信息 const 属性 左/右值属性尽可能正确地向下一层传递。三十二、这一套机制和 emplace 有什么关系后面我们会看到templateclass...Argsvoidemplace_back(Args...args);然后内部newT(std::forwardArgs(args)...);所以转发引用 引用折叠 std::forward 可变参数模板共同构成emplace make_shared 工厂函数 泛型包装器等现代 C 技术的基础。三十三、常见错误1. 认为右值引用变量始终是右值错误intx10;Func(x);x表达式是左值。2. 认为 std::move 会真正移动对象不准确。真正资源转移发生在移动构造 移动赋值 其他接受右值引用的函数里面。3. move 后继续依赖原来的值例如string shello;string xmove(s);couts;s仍然是有效对象但不应该依赖它仍然等于hello。4. 在转发函数中无脑 move错误templateclassTvoidFunc(Tx){Target(move(x));}这样左值也会被强行移动。应该Target(forwardT(x));5. 返回局部对象的右值引用错误stringFunc(){string s;returnmove(s);}函数结束以后s 已经销毁返回引用会悬空。正常应该stringFunc(){string s;returns;}让返回值优化和移动语义发挥作用。三十四、知识体系总结C11 移动语义 | 左值 / 右值 | ┌───────────┴───────────┐ ↓ ↓ T T 左值引用 右值引用 | std::move | 移动构造 / 移动赋值 | 避免深拷贝 T 模板推导 | 引用折叠 | 转发引用 | std::forward | 完美转发总结这一章真正需要掌握的不是多记一个 而是理解 C11 建立了一整套对象资源转移机制核心链条右值 ↓ 右值引用 ↓ 移动构造 / 移动赋值 ↓ 减少不必要的深拷贝同时模板中T ↓ 引用折叠 ↓ 转发引用 ↓ std::forward ↓ 完美转发进一步为后面的可变参数模板 emplace 智能指针工厂 泛型库奠定基础。重点记住std::move 无条件转换成可移动的值类别 std::forward 有条件保持原始值类别以及有名字的右值引用变量 在表达式中本身是左值这三点如果真正理解右值引用这部分就已经打通了大半。建议白底、蓝色箭头资源转移用绿色深拷贝用橙色。
返回列表