关于 C++ 11 的几个小特性

Google 内部 C++ code 已经使用了 C++ 11 的标准,前面的 styleguide 上我们的确也看到了一些 C++ 11 相关的特性。不过更多的可能是不允许被使用的 😦 这里讨论几个有意思的东西。

smart_ptr 究竟应该传值还是引用?

其实比较集中的讨论是 shared_ptr,unique_ptr 大家都知道只能传值,这样调用方丧失对这个对象的所有权,这才叫 unique_ptr,引用本身是一种分享 ownership 的传递方法,不过注意和指针不同的是,传递引用表示被调用的一方并不拥有这个对象,当然它可以另外保存一份。指针之所以被人讨厌,一方面是释放容易被忘记,另一方面就是所有权表示的非常模糊。Qt 和 Gtkmm 的争执曾经就集中在这一点上。 那么有了 smart_ptr 似乎语义上这个稀缺的东西被弥补上了,那么问题是 shared_ptr 本身到底应该传递值还是引用呢?有人很坚定的说,当然是 const ref 这样避免了对象的 copy,效率高呀!可是这是真的吗?事实上,诚如这篇讨论里面所说,shared_ptr 是一个 move 相对 copy 更加 cheap 的对象,因为很明显 move 操作时原指针最后会消亡而目的指针才是最终的拥有者,这个 count 一来一去不需要变化,而如果仅仅是通过 const ref 传递然后进行 copy constructor 的话,很明显其中额外的 count 增减将会降低效率。 Google 的 style guide 其中很诡异的一点是要求函数的输入传递 const ref 输出通过指针,这一点上个人觉得可能有问题的:

  • 好处可能是书写的时候因为要传递指针,输出往往因为附加了 & 所以看起来谁是输入谁是输出还算比较明显,例如 foo(bar, &baz)
  • 坏处自然就是实现 foo 的人似乎还得对参数做 null check,对对象修改的时候得用 -> 而不能直接用 .
  • 不能更好的利用 c++11 的 move semantics,其实 return 对象的代价可能并不那么高…

不幸的是,google styleguide 也 ban 掉了 move…

在有 rvalue/lvalue 的情况下怎样正确的书写相关函数?

我们知道所谓的 rvalue 是只能出现在等号右侧的 expression,常见的有常数、临时变量等;相对而言的 lvalue 是可以出现在等号左侧的 expression,注意这里的措辞是“可以”,也就是说 lvalue 是允许出现在等号右侧的。在早前的 C++ 标准中,没有语法可以表达这种细微的区分,因此写程序的时候有一定的限制。比如一个典型的问题是 copy constructor/operator=,通常如果传递的是一个 rvalue(如临时的变量),如果行为仍然是 copy 的话,那么临时对象自己管理的 resource 就需要被复制,同时还会被释放,看起来是非常不经济的做法,但是不论是传递值、引用或者是 const ref 都不能准确的表达这个含义。

为此 C++11 引入 rvalue reference 这样一个语法,但是它表达仅仅匹配 rvalue 的 reference。我们知道原有的语法中通过 int& 可以匹配 lvalue,但是无法匹配 rvalue,如果使用 const int& 可以匹配 rvalue,却只能将 rvalue 作为输入但却无法修改其内容,在 C++11 里面可以写 int&& 匹配 rvalue 的引用,这样与 const int& 共存的情况下,int&& 匹配了正好需要 rvalue 的情形(如需要 move semantics)。

不过即便这样还是不够的,其实 rvalue 一个重要的特点除了仅仅只能出现在等号右边以外,就是没有名字,试想如果写了一个函数输入是 int&& a 了,那岂不是 a 自己也成为了一个名字。因此从某种意义上如果希望将 a 传递给另一个重载过可以接受 lvalue/rvalue ref 的函数,你就需要将其名字去掉,std::move 就是干这个事情,将给定变量强制转换到 rvalue。

C++11 关于引用更大的一个转变是引入了 reference collapse 机制。早先模板参数如果写 T,那么传值,如果写 T& 则传引用,更多的可能是传递 const T&(避免 rvalue 不能匹配)。一个常见的问题是如果我们要写一个函数将参数 forward 到一个可能接受const 也可能接受 non const ref 的函数里面,那岂不是每个参数都有 2 种选择,个数一多就得写 2^n 种情形分别特化?另如果写 T& & 就会导致编译错误,C++11 允许引用的 collapse,类似乘法,& 是 0 而 && 是 1,这意味着如果我们将模板传递的类型改为 T&&,这时其实无关 const 同时能 T 匹配上类型 X 的 rvalue ref 和其引用 X&(因为 T = X 的话 T&& 就是 X&&,而如果 T = X& 的话,T&& 就是 X& 了),这时我们需要一个类似 std::move 的东西将参数从可能的 rvalue ref 类型转换回 rvalue ref,但是如果不是 rvalue ref 就不必要转换,这个就是 std::forward 所实现的功能。

怎样实现比较合理的赋值行为?

有些时候某些赋值行为是很有意思的,需要 share 几个对象公共的部分是一个比较常见的 case,而如果你需要这样的功能,常见的 pattern 就是使用 std::shared_ptr。有些时候一旦你 identify 了这种 structure 实现起来就容易许多。比如我们设计一个 workflow 的库,我们称里面几个部分为 step,其中导致 step transition 的操作称为 transform,这种结构往往形成一个 dag。简单起见我们认为每个 step 结果只有一个,这样意味着 step 本身可以看成是一个 lazy evaluated variable,那么如果你希望一个方法能将若干个 step 组合起来,就会面临一个小小的问题:

transform1 t1 ;
transform2 t2 ;
transform3 t3 ;

auto s0 = read ("somewhere") ;
auto s1 = step (t1, s0) ;
auto s2 = step (t2, s1) ;
auto s3 = step (t3, s1, s2) ;
return s3 ;

当我们构造后续的 steps 的时候,我们需要保存“值”而不是“引用”,离开这个 scope 前面的 steps 就消亡了,因此 s3 里面需要保存 s0、s1、s2 对应的副本,同时也应该看到这些副本对应的 lazy variable 是与原变量共享的,这样才不至于 s3 已经 evaluate 过了,如果检查 s1 之类的还没有值(因为只是其副本被 evaluate 过了)。

——————–
Thirty milch camels with their colts, forty kine, and ten bulls, twenty she asses, and ten foals.

Advertisements
关于 C++ 11 的几个小特性

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s