加载中...
C++的移动语义是怎么实现的?
第1节:代码改善:一个“坑爹”的文字类冒险游戏
第2节:在禁止多重继承的情况下,如何设计“直立智慧猩猩”类?
第3节:C++多线程代码中的“乱序”执行现象
第4节:C++中函数指针有什么作用呢?
第5节:为什么我用c++写的游戏那么简陋?
第6节:多线程读写socket导致的数据混乱的原因是什么?
第7节:WebSocket 是什么原理?为什么可以实现持久连接?
第8节:怎样在c++中实现instanceof?
第9节:一个函数多处 return 是好风格吗?
第10节:C++中虚函数相比非虚函数的优势
第11节:为什么 C::C::C::C::foo() 能编译成功?
第12节:如何静态反射C++枚举的名字
第13节:看C++大叔如何拥 java 妹子入怀……
第14节:坨——理解递归实现“汉诺塔”代码的关键
第15节:C++编译器如何实现 const(常量)?
第16节:C++如何为断言加上消息
第17节:初学C++到什么水平,算是合格的初级开发工程师?
第18节:C++编程要避免使用单例模式吗?
第19节:学习C++要学boost库吗?
第20节:C++的继承就是复制吗?
第21节:C++构造函数失败,如何中止创建对象?
第22节:C++学完多线程后,学什么呢?
第23节:string_view 适合用做函数的返回值类型吗?
第24节:为指针取别名,为何影响const属性?
第25节:std::enable_shared_from_this 的存在意义?
第26节:C++模板可变参数如何一次性解包?
第27节:Linux下的c++开发,平时是怎么调试代码的呢?
第28节:C++的移动语义是怎么实现的?
课文封面

理解C++11中用于支持移动语义的 std::move () 库函数所做的事,以及编译器区分不同情况所能做的事。包括 6 种情况。

  1. 低阶穷鬼型
  2. 豪横霸道型
  3. 钉子户型
  4. 趁人将危型
  5. 主动型
  6. 教化进步型

问:写了很多 C++,很多次用到智能指针、右值引用、完美转发,但还是感觉对移动语义没有很深的理解。我现在大概懂的是 std::move 的作用是 cast 成右值。 但是具体编译器是怎么实现移动语义的呢? 是语义分析的时候修改变量名到地址映射的表吗?(对编译原理没有很深入的了解)

0 楔

语言要添加一种新功能,有时候需要编译器知道,有时候不需要编译知道。
右值引用,是一项需要编译器知道的功能。编译器怎么知道?得动语法。
假设让我来设计,我决定增加新的操作符: <~ 。

a <~ b; // 以 “快”为第一原则,把 b 的内容转给 a 。

那么怎么才能快呢? 因为我只负责定义语法,不负责实现,所以这项事,就交给编译了。

简单的说,“英明”的我,仅用了 两个符号,就达成 “让编译器知道” 这个目标。

虽然但是,这语法会和 a < ~b 冲突……

C++ 标准委员就没我“英明”了。他们让:static_cast<T>(v)const_vast<T> (v) 是关键字,但在涉及 “把它 case 成右值” 的这件事时,我以为他们会来一个 “move_cast<T>(v) ”……

没想到,他们第一不想再占用关键字,第二也不想要 “cast ”这个词了。于是多了一个标准库的工具函数:std::move<T> (arg) 。大致实现如下:

template<typename T> constexpr typename std::remove_reference<T>::type&& move(T&& arg) noexcept { return static_cast<typename std::remove_reference<T>::type&&>(arg); }

看吧,这函数干的事是不是就是一个 static_cast <> 调用的事? 只干一行事的函数,也是函数。使用函数有什么不好吗?

  • 第一,函数就得遵守函数的基本法,有它的语法副作用:作为函数入参 arg ,如果它真是在参数列表中直接构造的临时变量,那么它的生命周期延长不了。这C++的一个老问题:在函数参数中构造对象。还好,通常只有新手会掉进去。

  • 第二,容易带来理解方面不直观的问题:既然是函数(function),本能的会以为是这个函数实现了 “move/转移”,而不会想到,它其实只是在做 cast (类型转换)。还好,通常只有新手会这样联想。

我写 C++ 才 30年,新手一个,所以第一眼看 std::move(),也以为这个函数真的 “移动”了些什么。

后来看了上面的代码,才发现不是。原来是转发给 static_cast,借它把入参“标识”为右值引用类型(目的就是为了让编译器知道),于是我在 《白话 C++ 》提出建议,应该先在UNICODE里,发明出一个 汉字 “拆” 外面加个圈的新符号,再把它作为 C++ 表达 move 的关键字(或操作符)。

由于 C++ 标准委员会完全不听劝,UNICODE 相关组织不懂东方文化,所以这个字到现在也没有被发明出来——本来至少有机会在我的书中存在,但出版社又说什么敏感啊什么什么的……我就用 “〇拆” 来示例吧:

a = 〇拆 b;

人类(哪怕是新手)一看就懂,哦!这 b 要被拆了……

编译器头回看到中文符号和中文组成的操作符,不仅能一眼认出,很可能还会感到莫名的兴奋,从而加速编译过程。

尽管 <~ 和 〇拆 都没纳入标准带给天下所有C++程序员莫大的遗憾与伤害;但这只是对人而言。对编译器来说,它没有多少时间去遗憾或伤感,因为对它来说, “知道” 只是一个开始。


现实中的 “拆 ” ,是催毁一块地皮上的原有楼房,然后占用这个块地皮,在上面建更好的楼——哪怕……哦,没有哪怕。

C++代码中的 “拆”,是看到现有 某块内存M 上有某些数据 D,并且它现在归属于某个变量 V1,这时有另外一个变量 V2,它也想快速的(最低代价地)拥有 D——哪怕,让V1去死。

“哪怕,让它去死……”
“哪怕,让它去死……”
“哪怕,让它去死……”

说的易,做的难。拆迁工作挺难的,所以编译器必须认真区分情况——

情况一:低阶穷鬼型

低阶指C++语言中的基础数据类型,它们拥有的内容,通常复制成本极低(价值不高,谓之穷)。

int b = 5; int a = std::move(b);

这是一段合法的代码。但编译器其实什么额外的实现都没做。如果编译器能说话,那么此时它说的话是: “爷,您不就是要个 5 嘛?复制一个 5 没有任何成本啊……何必去抢别人的?我复制一个给您就是 ……”

所以,情况一的第二行代码,和 int a = b; // 复制一份 在实际效果上完全一样。

为什么要说 “在实际效果上完全一样”,而不说 “就是完全一样”?

因为正常的编译器就是把复制一个 5 给了 a,然后根本不会去理 b 这个变量 ;但遇到心理阴暗的编译器,在执行 std::move ( b ) 后,把 b 的值改为 0 或 1745752329,也是合法的……让它去死都可以了,改改它拥有的内容,当然合法。

反过来说,按 C++ 语法规定,当 std::move( b ) 以后,在语意上,应该认为 b 已经法律死亡了,不可用了……虽然、但是、其实,它好好的,毫发无损,当然可以继续用。

情况二:豪横土霸王型

一个整数你看不起,觉得人家穷,100个整数呢?

int b100 [100] = {1, 2, 3, 4, 5, 6, 7, 0}; int a100 [100]; a100 = std::move(b100);

编译器处理以上代码时,会不会执行 “move/ 转移” 呢?
好吧,你回答 “会” 或 “不会” 都是错的。

处理以上代码时,编译器被揍得鼻青脸肿,别说“拆”,哪怕你就想看一眼,然后原样复制也不行的呀!为什么呢?因为它违反 “地方法”了……

int b100 [100] = {1, 2, 3, 4, 5, 6, 7, 0}; int a100 [100]; a100 = b100; // 豪横 C 数组:老子让你复制了吗!

在 C 语言的语法里,原生数组就是这么霸道,不让复制!难道,就不能借革新进步之说,趁加入 “右值” 引用的机会,好好杀一杀原生数组这不讲道理的霸气吗?

int a100 [100] = std::move(b100); // 新法!

皇帝:

朕要颁布新法!允许通过最新的 std::move () 库函数,实现对原生数组元素的所有权转移!!哈哈~,朕要借此机会,把江南豪绅地主的财产,统统合法地转移到国库!!!!

众大臣:

皇上,祖宗之法不可移啊!

皇帝:

我没有修改祖宗之法!我只是加了条新法。祖宗不是说了嘛,“对修改封闭,对扩展开放”,朕这就是扩展!

众大臣齐刷刷跪下:

std::move() 是函数,而原生数组作为函数入参需退化为指针,这是 C 高祖所立之法,运行至今已近一甲子,万万不可修改,万万不可修改!

—— 当初要是听我的,使用 <~ 或 〇拆 来制定新规,这班大臣不就可以闭嘴了??

情况三:“钉子户”型

简单的基础类型不值一“拆”,豪横的数组类型又不让拆……在朕的江山里,复合类型除了数组,不还有结构体吗? 结构体怎样?我要拆它!

struct A { int a, b ,c, d , e; int f [100]; }; void test() { A a1 {1, 2, 3, 4, 5, {100, 200, 300, 0}}; A a2 = std::move(a1); }

那 a1 一看就很有料,可惜 ,编译器一样根本没实际动到 a1,仍然是一五一十地,复制了 105 个整数合计至少 420 个字节!这回成本可不低!

皇帝:

大胆逆臣!我让你把它家的财产 move 过来!!是 move ,不是 copy !!你这是欺君!!!

大臣:

非臣不想,是臣不能!这 a1 家中虽然人财众多,但迁移当头,从上到下,从主到仆,竟个个都反对搬迁……

皇帝:

我一国之君,就称动不了一个结构体的内容了?

编译器:

你个戏精,还真以为自己是一国之君,不就是个破写程序的吗?

皇帝:

大胆,我好像听到了短剧的什么画外音?

情况四:“趁人将死,夺人家产”型

大臣:

皇上,微臣倒是有一法:只需判他死型,我们就能明正言顺地夺他家之产,娶他家之妇……

皇帝:

拿什么名头判它死刑?赶紧说!

大臣:

把他作为一个函数的返回值,依我 大 C 之法,此时他即必死之人。

皇帝:不要使用引用格式了,快演示给朕看!

#include <iostream> struct A { int a, b ,c, d , e; int f [100]; std::string s; }; A r() { A a1 {1, 2, 3, 4, 5, {100, 200, 300, 0}, "ABC"}; std::cout << &a1 << std::endl; return a1; } int main() { auto a2 = r(); std::cout << &a2 << std::endl; }

皇帝:怎么不见 move() 调用?
大臣:我大C++朝新规:凡遇此类将死之人,皆可自动执行 “迁移”夺魂法……
皇帝:哦!那,怎么证明朕调用 r () 之后得到的这些钱,以及这些妇(战术性咳嗽两声),确实不是复制品?

大臣:我输出 a1 和 a2 的地址,您看:

0x7ffc0fef8bb0
0x7ffc0fef8bb0

完全一样!

皇帝:哈哈哈,朕还需要你来证明?!昨夜一宿……朕早有所察,确实原汁原味!
大臣:吾皇英明!
皇帝:英明个屁!判人死刑,夺人之妻,这是一个英明皇上所做的?我要的是天下归心,人人主动上献!偌大江山,就没有愿意主动奉献的类型吗?

情况五:主动上献型

大臣:皇上还需注重龙体,万勿伤心!我大C++江山,能做到主动奉献的数据类型,多如繁星!比如国家人才库里的 vector、string、皆是有杀身成仁精神的类型!

皇帝: show me your code to see see !

#include <iostream> #include <string> int main() { std::string sb = "ABCDEFGHDDDDDDDDAAAAA-LONNNNNNN"; std::string sa = std::move(sb); std::cout << "sb:" << sb << "\n" << "sa:" << sa << std::endl; }

大臣:这程序输出:

sb:
sa:ABCDEFGHDDDDDDDDAAAAA-LONNNNNNN

皇帝:呀!诚不欺我。这 sb 还真的奉献出了一切。
大臣:建议追封为义士!勿以原名呼之,以示尊崇。

皇帝:vector 呢?

#include <iostream> #include <vector> int main() { std::vector<int> v1 { 1, 2, 3, 4, 5}; auto v2 = std::move(v1); std::cout << "v1 size = " << v1.size() << "\nv2 size = " << v2.size(); }

输出:

v1 size = 0
v2 size = 5

皇帝:明白了,v1 的 size 都变成 0,确实是毫无保留啊!朕很欣慰,不过,尚有遗憾!

情况六:教化进步型

大臣:国库中人才济济,何憾之有?
皇帝:普通人定义的类型,就不能有这种转移之义吗?
大臣:这得靠教化!只要皇帝您在普天之下大兴教育,那么人人都能写出支持转移语义的类型。
皇帝:哦?那,这普天之下,可有哪家学堂之教育,称得上略得一二?
大臣:一我不知,三四臣亦不知道,若说二,有个网站叫 d2school.com 名为 “第2学堂”,做得还算有趣。
皇帝:让你打广告了吗?你,七步之内,马上写个既能复制,又能移动的类型给我看看。
大臣:微臣这就奉上:

#include <iostream> #include <iomanip> #include <memory> #include <tuple> class Moveable { struct Imp { int a; bool b; char c; int d[100]; }; public: Moveable() noexcept = default; ~Moveable() noexcept = default; Moveable(Moveable&& ) = default; Moveable(Moveable const& o) noexcept(false) : imp(new Imp(o.value())) { } Moveable& operator = (Moveable const& o) noexcept(false) { throw_if_moved(); o.throw_if_moved(); *imp = *o.imp; return *this; } private: void throw_if_moved() const noexcept(false) { if(!imp) { throw std::logic_error("This object has been moved away."); } } Imp const& value() const noexcept(false) { throw_if_moved(); return *imp; } public: static constexpr size_t SizeOfD() { return 100; } void SetABC(int a , bool b , char c) noexcept(false) { throw_if_moved(); imp->a = a; imp->b = b; imp->c = c; } void SetD(size_t index, int value) noexcept(false) { throw_if_moved(); if (index >= 100) { throw std::out_of_range("index out of [0, 100)"); } imp->d[index] = value; } private: std::unique_ptr<Imp> imp {new Imp}; friend std::ostream& operator << (std::ostream& os, Moveable const& m); }; std::ostream& operator << (std::ostream& os, Moveable const& m) { if(!m.imp) { os << "null"; return os; } os << "a:" << m.imp->a << ", b:" << std::boolalpha << m.imp->b << ", c:" << m.imp->c; os << ", d:{"; for (size_t i=0; i<4; ++i) { os << m.imp->d[i] << ", "; } os << "... }"; return os; } int main() { Moveable m1; m1.SetABC(64, false, 'A'); for (size_t i =0; i<Moveable::SizeOfD(); ++i) { m1.SetD(i, i); } std::cout << "m1 -> " << m1 << "\n"; Moveable m2 = m1; std::cout << "m2 -> " << m2 << "\n"; Moveable m3 = std::move(m1); std::cout << "m3 -> " << m3 << "\n"; std::cout << "after move : "; std::cout << "m1 -> " << m1 << "\n"; try { auto m4 = m1; } catch(std::exception const& e) { std::cout << e.what() << std::endl; } }

输出:

m1 -> a:64, b:false, c:A, d:{0, 1, 2, 3, ... }
m2 -> a:64, b:false, c:A, d:{0, 1, 2, 3, ... }
m3 -> a:64, b:false, c:A, d:{0, 1, 2, 3, ... }
after move : m1 -> null
This object has been moved away.

皇帝:光看哪行,朕想动手!
大臣:臣垂垂老矣,皇上请自重!
皇帝:我是说我要动手编译,运行,查看你写的这段例程!

在线运行