
① 钩子std::move(a)之后数据真的飞走了吗std::move(a)之后a的数据真的飞走了吗——没有。std::move只是一个类型转换把a标成右值引用的标签。真正的搬动发生在移动构造/移动赋值里偷指针 把源对象置空。换个说法move是许可证不是动作拿证之后干不干、怎么干由移动构造函数决定。这一集我们要在汇编里看清拷贝和移动的差别、为什么移动后源对象要置空、std::forward和std::move的区别。② 源码 vs 汇编对照structBuffer{int*data;size_t size;Buffer(size_t n):data(newint[n]),size(n){}~Buffer(){delete[]data;}// 拷贝构造深拷贝分配 整块复制__attribute__((noinline))Buffer(constBuffero):data(newint[o.size]),size(o.size){std::memcpy(data,o.data,size*sizeof(int));}// 移动构造只偷指针不碰数据__attribute__((noinline))Buffer(Buffero)noexcept:data(o.data),size(o.size){o.datanullptr;o.size0;}};voidtest(){Buffera(10);Buffer bstd::move(a);// 移动构造Buffer ca;// 拷贝构造深拷贝(void)b;(void)c;}__attribute__((noinline))Buffercopy_it(Buffer b){returnb;// C17按值参数返回 → 隐式移动}拷贝构造 —— 分配新数组 整块复制Buffer(Buffer const): ... leaq 0(,%r8,4), %rcx ; size * 4 字节 call _Znay ; 分配新数组operator new[] ... jmp memcpy ; 整个缓冲区复制过去移动构造 —— 6 条 mov零分配Buffer(Buffer): movq (%rdx), %rax ; o.data movq %rax, (%rcx) ; this-data o.data 偷指针 movq 8(%rdx), %rax ; o.size movq $0, (%rdx) ; o.data nullptr ← 关键把源置空 movq %rax, 8(%rcx) ; this-size o.size movq $0, 8(%rdx) ; o.size 0 rettest()里的调用证据call _Znay ; new int[10] 给 a call Buffer::Buffer(Buffer) ; b std::move(a) → 移动构造 call Buffer::Buffer(Buffer const) ; c a → 拷贝构造深拷贝 ...作用域结束三个对象依次析构各调用 delete[]copy_it—— 返回按值参数隐式移动copy_it(Buffer): call Buffer::Buffer(Buffer) ; 返回 b 时选的是移动构造不是拷贝 ret③ 为什么这么设计std::move(x)的本质一个static_castT(x)。它不搬任何数据只是改变值类别让编译器在重载决议时选中T(T)而不是T(const T)。移动构造才是动作把源对象的资源指针过户给新对象并把源对象置成空壳nullptr/0。这样源对象析构时不会delete一个已经被别人持有的指针——置空防止 double-free / 悬垂。为什么 C17 返回按值参数自动移动返回一个即将销毁的局部对象时标准规定编译器优先选移动构造隐式移动。copy_it里那条call move ctor就是标准行为在机器码上的体现。完美转发std::forward模板里需要把参数按调用者给的左/右值属性原样转发时用forward。它是带条件的 move参数本来是右值才转成右值否则保持左值。核心目的避免转发时把右值降级成左值导致多一次拷贝。它的汇编随模板实例化而定但原理就是保持值类别。④ 深入一把源置空的三种写法与后果移动构造里把源置空是义务但写多写少后果不同// 只偷指针不置空错误示范Buffer(Buffero):data(o.data),size(o.size){}// 后果o 析构时 delete[] data新对象持有一个已被释放的指针 → 双重释放/悬垂// 偷指针 置空正确Buffer(Buffero):data(o.data),size(o.size){o.datanullptr;o.size0;}// 后果o 析构时 delete[] nullptr安全新对象独占资源置空的机器理由析构函数无条件delete[] data。如果不置空源对象析构时会把已经过继给别人的资源释放掉——这正是移动后源对象处于合法但未指定状态的标准含义。移动构造的 6 条 mov 里那两条movq $0是最关键的它们是防止双重释放的保险。⑤ 深入二移动赋值与自赋值移动赋值比移动构造多一步释放自己旧资源Bufferoperator(Buffero)noexcept{if(this!o){// 自赋值保护delete[]data;// 先释放自己的旧资源datao.data;o.sizeo.size;o.datanullptr;o.size0;}return*this;}自赋值检查a std::move(a)时若先delete[] data再把o.data赋给自己等于把刚删的指针又拿回来——UB。所以先释放再偷必须防自赋值。汇编形态多了cmpq %rdi, %rsithis vs o 条件跳过 delete[]调用。移动赋值通常比移动构造贵多一次释放旧资源。⑥ 深入三为什么别对返回值 moveBuffermake(){Bufferb(10);returnb;// 编译器做 NRVOE05——零拷贝// return std::move(b); // 反优化显式 move 会破坏 NRVO强制移动构造}return b;b 是命名局部对象→ NRVO直接在返回值位置构造零构造调用return std::move(b);→ 显式把 b 标成右值编译器必须走移动构造无法省略多一次调用。所以实战规则返回局部对象就裸return别加std::move——后者把可省略变成必须移动是反优化。⑦ 常见误区误区 1“std::move把数据搬走了”它只是static_castT。真正搬的是移动构造。误区 2“移动总是比拷贝快”对 PODint、double、普通 struct移动 复制无收益只有持有可分离资源的类型才获益。误区 3“移动后源对象还能用”标准只说合法但未指定通常是被置空的空壳。别在移动后假设源对象内容还在。误区 4“所有类型都能移动”没写移动构造、且不可拷贝delete的类型std::move会编译失败只有可移动类型才支持。误区 5“forward和move可以互换”forward是条件 move保持参数原始值类别move是无条件转右值。模板转发用forward具体对象交接用move。误区 6“移动构造不写也行编译器会自动生成”编译器会自动生成隐式移动构造但条件是类没有自定义析构、拷贝构造、拷贝赋值、移动赋值中任何一个。你一旦写了析构比如Buffer隐式移动构造就不会生成——所以有析构就要自己写移动否则退化成拷贝或不可移动。误区 7“移动语义只在 C11 后才有价值”价值来自持有可分离资源的类型。std::string/std::vector/智能指针都大量受益POD 结构仍无感。判断标准不是语言版本而是这个类型有没有可偷的堆资源。误区 8“移动后源对象内存还在数据也还在”内存可能在栈/堆对象没被销毁但内容不保证——好的移动实现置空垃圾实现可能残留原值但语义上未指定。永远别在移动后读源对象的数据先赋值再使用。⑧ 实战启示移动 ≠ 免费对int、POD、基本类型移动 复制没有可偷的资源。只有当对象持有可分离的资源堆指针、句柄、文件描述符时移动才有收益。写移动构造/赋值时三件事① 转移资源②把源置空③ 标noexcept——否则vector扩容时看到可能抛异常的移动会退化回用拷贝见 E14。别到处std::move对没写移动语义的类型move无效甚至有害源对象被置空却没有资源可用。返回值用 RVO 就够了别 move 返回值会破坏 RVO。转发参数用std::forward不要无脑move模板里传参到底该move还是forward取决于这参数还要不要保留值类别。⑨ 扩展专题一值类别value category速览C11 起值类别是左值/右值的精细划分理解move/forward的前提类别例子能否绑定到左值 lvalue具名变量、返回引用的表达式T、const T纯右值 prvalue临时对象、字面量、return 表达式T、const T亡值 xvaluestd::move(x)、返回右值引用的表达式T、const Tstd::move(x)把左值x变成xvalue“即将消亡的值”移动构造/赋值接收Txvalue/prvalue 都能绑定拷贝构造接收const T左值也能绑定。重载决议给左值和右值分别重载就能区分拷贝和移动。std::move的唯一作用就是让编译器选中右值重载。这个值类别→重载→选移动的链条就是移动语义的全部机关。⑩ 扩展专题二完美转发的汇编长什么样std::forward的汇编其实就是参数原样传递templateclassTvoidouter(Tt){inner(std::forwardT(t));}T是转发引用也叫万能引用T 被推导后T要么是T左值要么是T右值std::forwardT(t)按 T 是否左值引用决定返回t还是std::move(t)汇编结果右值路径就是直接把寄存器值传给inner没有额外拷贝左值路径传地址。为什么需要 forward如果模板里直接inner(t)t 是具名左值右值参数会被降级成左值导致inner选择拷贝而非移动。forward恢复原始值类别保证右值一路右值——这就是完美转发的含义转发后参数的价值类别和调用者给的一模一样。⑪ 扩展 FAQQstd::move_if_noexcept是什么A移动不抛异常才转右值否则保持左值。std::vector扩容用它来决定敢不敢移动E14 详述。Q移动后源对象一定为空吗A不保证。标准只要求合法但未指定。好的实现置空但别依赖移动后为空来写业务逻辑。QT在模板里是右值引用吗A模板推导时是转发引用可能折叠成左值引用非模板、明确类型时才是右值引用。这是模板特有的引用折叠规则。Qstd::move到const引用会怎样Aconst T能绑定右值但拷贝/移动重载里没有const T的移动版本所以仍走拷贝。move对 const 对象无效安全但没收益。Q移动语义和 RVO 冲突吗A不冲突。RVO 是直接构造省略一切移动是接管资源。前者零构造后者一次移动。编译器优先 RVO不可 RVO 才退到移动。⑫ 扩展实验数 movg -O2 -S E12_move.cpp数移动构造里几条指令应约 6 条 mov拷贝构造里几条分配 memcpy。验证 move 是 caststd::is_same_vdecltype(std::move(x)), int在 static_assert 里确认。看 forward 折叠模板里对左值/右值分别调inner反汇编确认右值走移动、左值走拷贝。破坏 RVO 对比return b;vsreturn std::move(b);-O2 -S对比后者多出的移动构造调用。自赋值实验a std::move(a);有/无自赋值保护的移动赋值跑 UB可能崩验证保护必要。⑭ 扩展专题三为什么 vector 扩容依赖 noexcept 移动这是移动语义影响标准库行为最经典的例子。std::vector扩容时要把旧元素搬到新内存如果移动构造是noexcept直接移动偷指针快如果移动构造可能抛异常万一移动到一半抛异常旧容器已被破坏标准无法保证强异常安全——所以标准规定此时回退到拷贝拷贝失败可以回滚。structMoveable{Moveable(Moveable)noexcept;};// vector 扩容 → 移动structThrowy{Throwy(Throwy);};// 可能抛 → 扩容回退拷贝后果Throwy如果没写拷贝构造或拷贝很贵vector 扩容可能编译失败或极慢。这也解释了为什么标准库作者都强调移动构造务必标noexcept——它不只是承诺还直接决定 vector 扩容用移动还是拷贝。反汇编对比扩容循环里一个是call move ctor6 条 mov一个是call copy ctor分配 memcpy数量级不同。⑮ 扩展专题四移动语义在 lambda/返回中的配合把本集和 E11 连起来看lambda移动捕获[x std::move(big)]闭包成员用移动构造偷资源避免拷贝大对象——E11 提过的移动捕获到这里落地函数返回大对象RVO 优先 → 不可省略时移动 → 最差拷贝。返回值优化的优先级链是 C 性能心法容器push_back一个移动对象v.emplace_back(std::move(big))只搬指针push_back(big)则拷贝——这就是emplace move组合的意义。一条主线从 E05 的 RVO、E11 的闭包捕获到本集的移动构造全是同一个主题——值传递如何被编译器/语言特性压到接近零成本。理解了这条链你就掌握了现代 C 性能的第一性原理。再往下推一步移动语义的收益要兑现还依赖调用方正确使用std::move/forward和类型正确声明noexcept移动。任何一个环节断了没 move、没 noexcept、误用 forward收益就漏掉。这也是现代 C 性能优化需要系统性理解的原因——不是某一招而是整条链。⑯ 扩展 FAQ第二轮Qstd::move一个std::string真的快吗Astd::string通常用 SSOE14 详述短字符串存在对象内部16 字节内移动 复制内部缓冲和拷贝一样快长字符串才偷指针 置空。所以移动 string的收益取决于长度是否超 SSO。Q移动语义能用于内置数组吗A数组不能整体移动不能int a[10] std::move(b)但std::array可以它是聚合包装。数组移动是逐元素。Qstd::swap和移动的关系Astd::swap对可移动类型用移动三次实现tmpmove(a); amove(b); bmove(tmp)比拷贝三次便宜——交换两个Buffer从3 次深拷贝变成3 次移动各 6 条 mov。这就是为什么现代swap依赖移动语义。Q为什么移动后置空是推荐而非强制A标准只要求合法但未指定——给实现自由度。但不置空的实现不安全析构会双释放所以实际都置空。这也是别在移动后继续用源对象的谨慎理由。Qstd::move对常量有作用吗A没有。const T没有移动语义移动构造接收非 const 右值引用std::move(const_obj)只会退到拷贝。所以移动要求源对象可修改。Q转发引用T在 C17 有什么坑A若调用者传左值T 推导为TT折叠成T——转发引用变成左值引用此时forward返回左值。这个折叠规则就是完美转发的数学基础。⑰ 扩展实验第二轮noexcept 开关对比给移动构造加/去noexcept用std::vectorBuffer扩容触发反汇编对比移动 vs 拷贝的扩容路径。string SSO 移动std::string短串和长串分别 move-O2 -S看是内部缓冲复制还是偷指针置空。emplace vs push_backv.emplace_back(std::move(b))vsv.push_back(b)反汇编对比移动/拷贝构造调用次数。lambda 移动捕获[x std::move(big)]闭包构造反汇编确认走移动构造。value category 打印用decltypeis_lvalue_reference/is_rvalue_reference打出一组表达式的值类别把 ⑨ 的表变成代码。⑲ 扩展专题五移动构造的完整检查清单写一个正确的移动构造/移动赋值可以按清单逐项核对转移资源把o的堆指针/句柄赋给this偷指针置空源对象o.data nullptr; o.size 0;防止源析构时双重释放标noexcept让std::vector扩容敢用移动E14处理自赋值移动赋值特有if (this ! o)再动手释放旧资源移动赋值特有先delete自己的旧数据再偷对称性移动后源对象合法但未指定别再依赖其内容。对照本集汇编前两条对应 6 条 mov第三条影响是否被 vector 调用第四/五条在移动赋值里是额外指令。写对移动构造的收益vectorBuffer扩容从拷贝分配memcpy“变成移动6 条 mov”——数量级差异。⑳ 扩展专题六std::optional 与移动/拷贝的配合std::optionalT在内部可能没有 T地存一个 T它的移动语义分两层std::optionalBufferaBuffer(10);// 构造内部 Bufferstd::optionalBufferbstd::move(a);// 移动optional 内部 T 被移动构造optional自己通常 trivially movable内部一个 bool 标志 对齐缓冲移动optionalBuffer时内部Buffer走移动构造6 条 movbool 标志照搬所以optional的移动成本 ≈ 内部 T 的移动成本 一条标志复制。启示现代 C 的类型组合optional/variant/容器的移动成本等于各成员移动成本之和——理解了 E07 的逐成员和本集的移动偷指针就能预算任意组合类型的移动开销。㉑ 扩展 FAQ第三轮Q移动构造能省略吗A能。RVO/NRVO 时直接构造在目标位置移动构造也不调用E05 讲过。移动是无法省略时的次优解RVO 是最优解。Q为什么返回局部对象会自动移动而不是拷贝A标准规定返回即将销毁的具名局部对象时优先移动隐式移动规则。copy_it里call move ctor就是证据。Qstd::forward一定要在模板里用吗A是的它的设计就是配合转发引用模板推导。非模板里forward退化成普通 cast没意义——用move更直白。Qstd::unique_ptr为什么只有移动没有拷贝A拷贝它会让两个指针指向同一块内存双释放所以拷贝构造被delete只能移动转移所有权。这是移动语义天然适配独占所有权的例证E13 展开。Q移动一个对象后还能重新给它赋值用吗A可以。合法但未指定允许重新赋值但读它的旧内容是不可靠的。先赋值/构造新值再使用是安全的。㉒ 扩展实验第三轮移动赋值自赋值a std::move(a)带/不带自赋值保护各跑一次观察是否崩溃/UB。optional 移动std::optionalBuffer移动的反汇编确认内部 Buffer 走移动构造。隐式移动规则函数返回按值参数与局部对象反汇编对比移动构造调用情况。noexcept 陷阱移动构造不标 noexcept 的类放进std::vector看编译器是否退化为拷贝用日志/反汇编。资源所有权转移链unique_ptr从函数返回、存容器、再 move全程确认零拷贝、只搬指针E13 预告。㉓ 悬念被移动、被拷贝的堆内存谁来替我们安全地管理裸new/delete太容易出错。