问:写了很多 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.
皇帝:光看哪行,朕想动手!
大臣:臣垂垂老矣,皇上请自重!
皇帝:我是说我要动手编译,运行,查看你写的这段例程!