现在的位置: 首页 > 综合 > 正文

《C++0x漫谈》系列之:右值引用(或“move语意与完美转发”)(上)

2013年06月10日 ⁄ 综合 ⁄ 共 3384字 ⁄ 字号 评论关闭

C++0x漫谈》系列之:右值引用

或“move语意与完美转发”(上)

 

By 刘未鹏(pongba)

刘言|C++的罗浮宫(http://blog.csdn.net/pongba)

 

 

C++0x漫谈》系列导言

 

这个系列其实早就想写了,断断续续关注C++0x也大约有两年余了,其间看着各个重要proposals一路review过来:rvalue-referencesconceptsmemory-modelvariadic-templatestemplate-aliasesauto/decltypeGCinitializer-lists…

 

总的来说C++09C++98相比的变化是极其重大的。这个变化体现在三个方面,一个是形式上的变化,即在编码形式层面的支持,也就是对应我们所谓的编程范式(paradigm)C++09不会引入新的编程范式,但在对泛型编程(GP)这个范式的支持上会得到质的提高:conceptsvariadic-templatesauto/decltypetemplate-aliasesinitializer-lists皆属于这类特性。另一个是内在的变化,即并非代码组织表达方面的,memory-modelGC属于这一类。最后一个是既有形式又有内在的,r-value references属于这类。

 

这个系列如果能够写下去,会陆续将C++09的新特性介绍出来。鉴于已经有许多牛人写了很多很好的tutor这里这里,还有C++标准主页上的一些introductiveproposals,如这里,此外C++社群中老当益壮的Lawrence Crowl也在google做了非常漂亮的talk)。所以我就不作重复劳动了 :) ,我会尽量从一个宏观的层面,如特性引入的动机,特性引入过程中经历的修改,特性本身的最具代表性的使用场景,特性对编程范式的影响等方面进行介绍。至于细节,大家可以见每篇介绍末尾的延伸阅读。

 

 

右值引用导言

 

右值引用(及其支持的Move语意和完美转发)是C++0x将要加入的最重大语言特性之一,这点从该特性的提案在C++ - State of the Evolution列表上高居榜首也可以看得出来。从实践角度讲,它能够完美解决C++中长久以来为人所诟病的临时对象效率问题。从语言本身讲,它健全了C++中的引用类型在左值右值方面的缺陷。从库设计者的角度讲,它给库设计者又带来了一把利器。从库使用者的角度讲,不动一兵一卒便可以获得“免费的”效率提升

 

Move语意

 

返回值效率问题——返回值优化((N)RVO)——mojo设施——workaround——问题定义——Move语意——语言支持

 

大猴子Howard Hinnant写了一篇挺棒的tutoriala.k.a. 提案N2027),此外最初的关于rvalue-reference的若干篇提案的可读性也相当强。因此要想了解rvalue-reference的话,或者去看C++标准委员会网站上的系列提案(见文章末尾的参考文献)。或者阅读本文。

 

源起

《大史记》总看过吧?

 

故事,素介个样子滴一天,小嗖风风的吹着,在一个伸手不见黑夜的五指(哎哟,谁人扔滴板砖?!%$@

 

我用const引用来接受参数,却把临时变量一并吞掉了。我用非const引用来接受参数,却把const左值落下了。于是乎,我就在标准的每个角落寻找解决方案,我靠!我被8.5.3打败了!

 

设想这样一段代码(既然大同小异,就直接从Andrei那篇著名的文章里面拿来了):

 

std::vector v = readFile();

 

readFile()的定义是这样的:

 

std::vector readFile()

{

  std::vector retv;

  … // fill retv

  return retv;

}

 

这段代码低效的地方在于那个返回的临时对象。一整个vector得被拷贝一遍,仅仅是为了传递其中的一组int,当v被构造完毕之后,这个临时对象便烟消云散。

 

这完全是公然的浪费!

 

更糟糕的是,原则上讲,这里有两份浪费。一,retvretvreadFile()结束之后便烟消云散)。二,返回的临时对象(返回的临时变量在v拷贝构造完毕之后也随即香消玉殒)。不过呢,对于上面的简单代码来说,大部分编译器都已经能够做到优化掉这两个对象,直接把那个retv创建到接受返回值的对象,即v中去。

 

实际上,临时对象的效率问题一直是C++中的一个被广为诟病的问题。这个问题是如此的著名,以至于标准不惜牺牲原本简洁的拷贝语意,在标准的12.8节悍然下诏允许优化掉在函数返回过程中产生的拷贝(即便那个拷贝构造函数有副作用也在所不惜!)。这就是所谓的“Copy Elision”。

 

为什么(N)RVO((Named) Return Value Optimization)几乎形同虚设

还是按照Andrei的说法,只要readFile()改成这样:

 

… readFile()

{

if(/* err condition */) return std::vector();

if(/* yet another err condition */) return std::vector(1, 0);

std::vector retv;

… // fill retv

return retv;

}

 

出现这种情况,编译器一般都会乖乖放弃优化。

 

但对编译器来说这还不是最郁闷的一种情况,最郁闷的是:

 

std::vector v;

v = readFile(); // assignment, not copy construction

 

这下由拷贝构造,变成了拷贝赋值。眼睛一眨,老母鸡变鸭。编译器只能缴械投降。因为标准只允许在拷贝构造的情况下进行(N)RVO

 

为什么库方案也不是生意经

C++鬼才Andrei Alexandrescu以对C++标准的深度挖掘和利用著名,早在03年的时候(当时所谓的临时变量效率问题已经在新闻组上闹了好一阵子了,相关的语言级别的解决方案也已经在029月份粉墨登场)就在现有标准(C++98)下硬是折腾出了一个能100%解决问题的方案来。

 

Andrei把这个框架叫做mojo,就像一层爽身粉一样,把它往现有类上面一洒,嘿嘿猜怎么着,不,不是“痱子去无踪” :P ,是该类型的临时对象效率问题就迎刃而解了!

 

Mojo的唯一的问题就是使用方法过于复杂。这个复杂度,很大程度上来源于标准中的一个措辞问题(C++标准就是这样,鬼知道哪个角落的一句话能够带出一个brilliant的解决方案来,同时,鬼知道哪个角落的一句话能够抹杀一个原本简洁的解决方案)。这个问题就是我前面提到过的8.5.3问题,目前已经由core language issue 391解决。

 

对于库方案来说,解决问题固然是首要的。但一个侵入性的,外带使用复杂性的方案必然是走不远的。因此虽然大家都不否认mojo是一个天才的方案,但实际使用中难免举步维艰。这也是为什么mojo并没有被工业化的原因。

 

为什么改用引用传参也等于痴人说梦

void readFile(vector& v){ … // fill v }

 

这当然可以。

 

但是如果遇到操作符重载呢?

 

string operator+(string const& s1, string const& s2);

 

而且,就算是对于readFile,原先的返回vector的版本支持

 

BOOST_FOREACH(int i, readFile()){

  … // do sth. with i

}

 

改成引用传参后,原本优雅的形式被破坏了,为了进行以上操作不得不引入一个新的名字,这个名字的存在只是为了应付被破坏的形式,一旦foreach操作结束它短暂的生命也随之结束:

 

vector v;

readFile(v);

 

BOOST_FOREACH(int I, v){

}

 

// v becomes useless here

 

还有什么问题吗?自己去发现吧。总之,利用引用传参是一个解决方案,但其能力有限,而且,其自身也会带来一些其它问题。终究不是一个优雅的办法。

 

问题是什么

《你的灯亮着吗?》里面漂亮地阐述了定义“问题是什么”的重要性。对于我们面临的临时对象的效率问题,这个问题同样重要。

 

简而言之,问题可以描述为:

 

C++没有区分copymove语意。

 

什么是move语意?记

抱歉!评论已关闭.