Untitled
raw download clone
TEXT
views 11
,
size 6216 b
Effective Modern C++是写于 C++14 版本的,现在已经发生了一些变化(在使用上变化不是很大,概念上变化比较大)。cppreference 定义照抄标准而且后面全是枚举例子,好像只看它很难看懂发生了什么。CppCon 里的好像很多是教你怎么用的,没咋看过,或许用多了也能发展出一些直觉吧。

为了理解它,如果不想在那 1800 多页的标准构成的粪坑里游泳的话,现成的材料里 C++ Templates: The Complete Guide 第二版关于 value categories 的附录可能好一点,就几页纸,概念又比较正确(不过 reference collapsing 放在正文中间的好像作者还建议第一次读跳过去的部份)。


简单来说大概是这么理解的(这只是我的理解方式,估计肯定有某些不符合标准的地方,另外可能先跳到 4 会好一点):

1. value categories 是表达式的属性而不是变量的属性或者别的什么玩意的属性。比如 int a = -1;这里的 a 并不是一个表达式,它不是啥左值,写在等号左边就是左值这个想法是错的。而 C++11 里为了 move semantics 等需求引入的 lvalue/rvalue reference 是两种不同的类型,它们是和表达式正交的属性,比如 void push_back(string&& x);里面的 x 的类型是右值引用 string&&,但函数定义里面如果直接写一个表达式 x 的话则是一个 glvalue 的表达式。

2. 自 C++17 起表达式分为两类:glvalue 和 prvalue ,前者提供某个位置信息,后者提供初始化或者修改的时候所需的值。从这个版本起只需要以上二者就可以理解我们需要的东西,不再需要另外定义 lvalue 和 rvalue 了。乍看之下存在一些显然有问题的地方,比如 int a = b;里面的 b 既提供了 b 这个变量的位置又提供了初始化 a 所需要的值。实际上 C++里存在以下两种 value categories 之间的直接转换(具体行为受到类型影响,但大致上符合直觉):
------2.1 lvalue-to-rvalue ,名字叫这个,但它实际上是把一个 glvalue 转换成一个 prvalue (正如前面所说,我们已经不需要 lvalue 和 rvalue 这两种 value categories 了),这个 prvalue 在用来提供值的时候会从转换成它的 glvalue 提供的位置信息里面来获取相应的值(不同的类型的具体行为不太一样,但大致上是符合直觉的)。比如之前提到的 int a = b;里面的 b 就是先转换成了 prvalue 才能提供用来初始化 a 的值的。
------2.2 temporary materialization conversion 机制允许通过一个 prvalue 生成一个临时对象然后把它的位置作为一个 glvalue 放在好像需要 glvalue 的地方,用这种机制可以在需要 const lvalue reference 的地方传进去一个 prvalue 。但这种机制发挥的场合是受限的,直觉上只有找不到 glvalue 让这个 prvalue 起到初始化的作用的时候才会发生这种转换。比如 string a = string(string(string()));里内层表达式是 prvalue ,它会一直往外抛,直到找到 glvalue a 后进行一次初始化,C++17 里利用这个特性实现了所谓的 guaranteed copy elision 。这种特性生成的 glvalue 表达式不能用在=的左边,也不能用来初始化 non-const lvalue reference ,由此我们在 glvalue 中分出一个新的子类 xvalue ,它只通过屈指可数的几种情况生成,感觉上更像是一种技术手段。

3. 作为 type 的 lvalue 和 rvalue reference 是与表达式的 value categories 正交的性质。但二者存在下面的相互作用:
------3.1 lvalue reference 只能被 glvalue 里面不是 xvalue 的那一类所初始化(这一类叫作 lvalue ),而 rvalue reference 只能被剩下的表达式初始化(即 xvalue 与 prvalue ,这一类叫作 rvalue ),并且不接受从 glvalue 转化来的 prvalue 。如果试图扔掉 lvalue/rvalue 的话,大概可以说 l/rvalue reference 只能被本来是 glvalue/prvalue 的表达式初始化,这么说看起来更简单,但我不知道能不能严格地定义。
------3.2 函数返回值的分类,或者说由调用这个函数构成的表达式的 value category ,受到返回值类型的影响:返回值为左值引用给出 lvalue ,右值引用给出 xvalue ,非引用类型给出 prvalue 。如果试图扔掉 lvalue/rvalue 的话大概思路和 3.1 一样沿着返回路径回溯看看源头是什么(碰到引用的时候看是谁初始化的引用类型),同样我也不知道能不能严格定义这种偷鸡理解。
------3.3 存在向 rvalue reference 的强制转换 std::move ,以及利用 reference collapsing (引用的引用会变成一个单层引用,左引用会传染下去)机制做的所谓 universal reference 与 perfect forwarding
------3.4 借由这种相互作用,我们可以通过表达式本身匹配上不同的函数调用 copy/move 相关的重载实现 move semantics ,也可以通过包含函数的表达式本质上的 value category 可以一直追溯到产生那个值的地方这个特性来实现 guaranteed copy elision 。

4. 总的来说细节很多,并且散布在标准的各处,它们之间还有相互作用,我已经看吐了(虽然吐过之后好像不怕读标准了),所以在不出意外的时候我打算用这个简化的理解:表达式分两类,一类提供广义的位置(最终用来取出来一个值或者修改一个值),一类提供广义的值(最终被某个位置吃掉),二者可以但只应该在必要的时候相互转化,毕竟从位置里取出来一个值或者用某些值创建一个临时对象是有代价的,我们尽量晚地做这件事,说不好可以少干一点或者干脆不用干了。左 /右引用是类型而不是表达式分类,但它们大致上可以通过追溯源头来反应表达式是哪类的:当源头是位置的时候引用的类型是左值引用,是值的时候引用的类型是右值引用,其中左值引用具有传染性,除非被某种转换(比如 std::move )截断。我们利用这个机制来实现 Modern C++里面的诸多特性。剩下的都是技术细节。
close fullscreen
Login or Register to edit or fork this paste. It's free.