资讯动态

xvalue 的引入,对学习 C++ 难度的影响(二)

发布时间:2026/11/6 6:42:38 来源:尧图企业网站定制
结论xvalue 本身不会让 C 难多少。让 C 变难的是移动语义这整套心智模型xvalue 只是它在类型系统里的记账单位。真正被它绊住的主要是写模板和库的人。为什么 xvalue 非存在不可这其实是个很简单的正交问题一个表达式是否指代已有的对象有没有身份它的资源是否允许被搬走这两件事本来互相独立。C03 的 lvalue / rvalue 两分法把它们捆在同一根轴上所以表达不了std::move(x)这种情形这仍然是x本尊地址、生命周期、别处指着它的指针都还在但请把我的资源当临时量搬走。于是有了第三格类别有身份可被搬走典型例子lvalue左值是否变量名x、*p、a[i]xvalue将亡值是是std::move(x)、返回T的函数调用prvalue纯右值否是42、T{}、返回T的函数调用另外两个名字只是并集glvalue lvalue ∪ xvalue有身份rvalue xvalue ∪ prvalue可搬走。所以准确的说法是3 个基本类别 2 个复合类别。「rvalue 里包含一种 glvalue」在字面上自相矛盾这是术语在 C11 定稿前才由 N3055 定型留下的命名债看起来难多半是这个原因。这套完整的体系主要服务于标准文本、编译器实现和极端的重载决议角落。顺带澄清一个常见误解std::move本身不移动任何东西它只是一个把表达式转成 xvalue 的 cast真正的移动发生在随后匹配到移动构造 / 移动赋值的时候。日常只需要记一个口诀具名的变量表达式是 lvalue要用std::move转成 xvalue才能匹配移动重载。voidfoo(constT);// (1)voidfoo(T);// (2)voidsink(Tx){foo(x);// x 有名字 → lvalue → 选 (1)foo(std::move(x));// 转成 xvalue → 选 (2)}注意x的类型是T但x这个表达式是 lvalue。这是新手最容易卡住的地方。这只是口诀有例外枚举项有名字却是 prvalue非类类型的非类型模板参数同理。入门够用但别把它当成语言规则本身。实战里真正咬人的坑这些坑基本都来自移动语义而不是 xvalue 这个概念const对象上的std::move静默退化成拷贝。const T绑不到T形参只能匹配const T没有任何警告是个性能暗坑。moved-from 对象的状态。标准库类型保证有效但未指定自定义类型只是约定俗成。编译器不会检查你是否在 move 之后继续使用它靠纪律clang-tidy的bugprone-use-after-move能抓一部分。模板推导、引用折叠、auto、decltype与 xvalue 交叉。对于不是裸标识符的表达式decltype的三条规则是prvalue 得Tlvalue 得Txvalue 得T。这和 C 最难的既有部分推导 重载决议叠在一起。return std::move(x)通常是负优化。返回局部变量或按值形参时编译器本来就会先把x当右值做重载决议隐式移动还有机会做拷贝省略手写std::move反而会阻止省略。但如果返回的是成员、全局变量等不在隐式移动范围内的东西std::move就是必要的。C20 的 P1825 和 C23 的 P2266 还在逐步放宽隐式移动的范围比如返回右值引用形参。生命周期。下面单独展开。生命周期哪些会延长哪些不会这是之前写得最不严谨的一点所以专门写成可编译的代码验证。规则先说结论生命周期延长发生在引用直接绑定到 prvalue 物化出的临时量或它的子对象时。延长挂在这个临时量上而不是挂在 xvalue 这个类别上。C17 的临时量物化把 prvalue 变成 xvalue但延长规则并不因此属于 xvalue。T r std::move(x);绑的是已经存在的x没有新的临时量谈不上延长x该死时照样死。只要引用的来源中间隔了一次返回引用的函数调用链条就断了。临时量在完整表达式结束时销毁引用悬垂。返回T和返回const T在这点上完全一样。对xvalue取成员结果仍是 xvalue不享受延长对prvalue取成员会延长整个临时量。代码用g 13.3、-stdc17 -Wall -Wextra编译通过。每个临时量的构造 / 析构都会打印所以能直接看出生死时刻。// 编译: g -stdc17 -Wall -Wextra -o lifetime lifetime.cpp#includecstdio#includeutilitystructNoisy{constchar*name;intvalue42;explicitNoisy(constchar*n):name(n){std::printf( ctor %s\n,name);}Noisy(constNoisy)delete;~Noisy(){std::printf( dtor %s\n,name);}};Noisymake(constchar*n){returnNoisy(n);}// 返回 prvalue (C17 保证省略)Noisypass(Noisyn){returnstd::move(n);}// 返回 xvalue,不会延长任何东西constNoisyidentity(constNoisyn){returnn;}// 对照组:返回 lvalue 引用// A. 引用直接绑定 prvalue:物化出的临时量被延长voidcase_a(){std::puts(-- A: const Noisy r make(\A\););{constNoisyrmake(A);std::printf( in scope, r.value %d\n,r.value);}std::puts( (scope ended));}// B. 右值引用绑定 prvalue:同样延长voidcase_b(){std::puts(-- B: Noisy r make(\B\););{Noisyrmake(B);std::printf( in scope, r.value %d\n,r.value);}std::puts( (scope ended));}// C. 引用绑定 prvalue 的成员子对象:整个临时量被延长voidcase_c(){std::puts(-- C: const int r make(\C\).value;);{constintrmake(C).value;std::printf( in scope, r %d\n,r);}std::puts( (scope ended));}// D. std::move 一个已有对象:没有新临时量,谈不上延长,r 只是 x 的别名voidcase_d(){std::puts(-- D: Noisy x(\D\); Noisy r std::move(x););{Noisyx(D);Noisyrstd::move(x);std::printf( r x ? %s\n,rx?yes:no);std::puts( leaving scope: only xs own dtor runs);}std::puts( (scope ended));}// E. 经过返回 T 的函数:临时量在完整表达式结束时就销毁,r 悬垂voidcase_e(){std::puts(-- E: Noisy r pass(make(\E\)); // 悬垂);{[[maybe_unused]]Noisyrpass(make(E));std::puts( next statement: temp is already gone, r dangles (do not use));}std::puts( (scope ended));}// F. std::move(prvalue):物化的临时量活不过这条语句voidcase_f(){std::puts(-- F: auto r std::move(make(\F\)); // 悬垂);{[[maybe_unused]]autorstd::move(make(F));std::puts( next statement: temp is already gone, r dangles (do not use));}std::puts( (scope ended));}// G. 对 xvalue 取成员:结果还是 xvalue,不享受延长voidcase_g(){std::puts(-- G: auto r std::move(make(\G\)).value; // 悬垂);{[[maybe_unused]]autorstd::move(make(G)).value;std::puts( next statement: temp is already gone, r dangles (do not use));}std::puts( (scope ended));}// H. 对照组:返回 const T 的函数同样不延长(lvalue 一侧的经典坑)voidcase_h(){std::puts(-- H: const Noisy r identity(make(\H\)); // 悬垂);{[[maybe_unused]]constNoisyridentity(make(H));std::puts( next statement: temp is already gone, r dangles (do not use));}std::puts( (scope ended));}intmain(){case_a();case_b();case_c();case_d();case_e();case_f();case_g();case_h();}悬垂的 E / F / H 三个例子里只让引用存在、绝不去读它所以程序本身没有触发未定义行为。GCC 13 会对其中三处给出-Wdangling-reference警告G 的情形它反而没报所以别指望编译器替你兜底。运行结果-- A: const Noisy r make(A); ctor A in scope, r.value 42 dtor A ← 作用域结束才析构被延长 (scope ended) -- B: Noisy r make(B); ctor B in scope, r.value 42 dtor B ← 被延长 (scope ended) -- C: const int r make(C).value; ctor C in scope, r 42 dtor C ← 绑成员子对象整个临时量被延长 (scope ended) -- D: Noisy x(D); Noisy r std::move(x); ctor D r x ? yes ← 只是 x 的别名没有新对象 leaving scope: only xs own dtor runs dtor D (scope ended) -- E: Noisy r pass(make(E)); // 悬垂 ctor E dtor E ← 这条语句一结束就析构了 next statement: temp is already gone, r dangles (do not use) (scope ended) -- F: auto r std::move(make(F)); // 悬垂 ctor F dtor F ← 同上 next statement: ... (scope ended) -- G: auto r std::move(make(G)).value; // 悬垂 ctor G dtor G ← xvalue 的成员不延长 next statement: ... (scope ended) -- H: const Noisy r identity(make(H)); // 悬垂 ctor H dtor H ← 对照组const T 返回值一样不延长 next statement: ... (scope ended)汇总写法是否延长原因const T r make();是直接绑定 prvalueT r make();是直接绑定 prvalueconst int r make().member;是绑定 prvalue 的子对象T r std::move(x);—不需要绑定已有对象r 只是别名T r pass(make());否悬垂隔了一次返回T的调用auto r std::move(make());否 悬垂std::move只是返回引用的函数auto r std::move(make()).member;否 悬垂xvalue 的成员仍是 xvalueconst T r identity(make());否 悬垂对照返回const T一样断链一句话记忆std::move是个普通函数它不会保住任何东西更不会替你延长什么。把 xvalue 当成能保命的东西才是真正的坑。按人群看难度增量差别很大业务代码开发者几乎碰不到 xvalue 这个词。只需要养成一个习惯交出所有权时move之后别再用它。模板 / 库作者难度明显提升。完美转发、引用折叠、decltype、生命周期并列是 subtle bug 的高发区。标准文本 / 编译器实现者确实大幅变难但普通开发者不用为此付费。另外C17 的保证复制省略简化了 prvalue 一侧的规则prvalue 不再先造临时量再移动而是直接初始化目标对象整体趋势其实是在收敛的。小结难度涨在所有权语义不涨在 xvalue 这个格子。把具名的是 lvalue、std::move只是 cast、const上 move 是拷贝、move 之后别再用、引用不会因为你move了就多活一会儿这几条记牢日常基本够用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑