刘未鹏|C++的罗浮宫

Knowledge sharing is the best reuse

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

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<int> v = readFile();

 

readFile()的定义是这样的:

 

std::vector<int> readFile()

{

  std::vector<int> 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<int>();

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

std::vector<int> retv;

… // fill retv

return retv;

}

 

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

 

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

 

std::vector<int> 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<int>& v){ … // fill v }

 

这当然可以。

 

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

 

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

 

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

 

BOOST_FOREACH(int i, readFile()){

  … // do sth. with i

}

 

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

 

vector<int> v;

readFile(v);

 

BOOST_FOREACH(int I, v){

}

 

// v becomes useless here

 

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

 

问题是什么

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

 

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

 

C++没有区分copymove语意。

 

什么是move语意?记得auto_ptr吗?auto_ptr在“拷贝”的时候其实并非严格意义上的拷贝。“拷贝”是要保留源对象不变,并基于它复制出一个新的对象出来。但auto_ptr的“拷贝”却会将源对象“掏空”,只留一个空壳——一次资源所有权的转移。

 

这就是move

 

Move语意的作用——效率优化

举个具体的例子,std::string的拷贝构造函数会做两件事情:一,根据源std::string对象的大小分配一段大小适当的缓冲区。二,将源std::string中的字符串拷贝过来。

 

// just for illustrating the idea, not the actual implementation

string::string(const string& o)

{

this->buffer_ = new buffer[o.length() + 1];

copy(o.begin(), o.end(), buffer_);

}

 

但是假设我们知道o是一个临时对象(比如是一个函数的返回值),即o不会再被其它地方用到,o的生命期会在它所处的full expression的结尾结束的话,我们便可以将o里面的资源偷过来:

 

string::string(temporary string& o)

{

// since o is a temporary, we can safely steal its resources without causing any problem

 

this->buffer_ = o.buffer_;

o.buffer_ = 0;

}

 

这里的temporary是一个捏造的关键字,其作用是使该构造函数区分出临时对象(即只有当参数是一个临时的string对象时,该构造函数才被调用)。

 

想想看,如果存在这样一个move constructor(搬移式构造函数)的话,所有源对象为临时对象的拷贝构造行为都可以简化为搬移式(move)构造。对于上面的string例子来说,movecopy construction之间的效率差是节省了一次O(n)的分配操作,一次O(n)的拷贝操作,一次O(1)的析构操作(被拷贝的那个临时对象的析构)。这里的效率提升是显而易见且显著的。

 

最后,要实现这一点,只需要我们具有判断左值右值的能力(比如前面设想的那个temporary关键字),从而针对源对象为临时对象的情况进行“偷”资源的行动。

 

Move语意的作用——使能(enabling)

再举一个例子,std::fstreamfstream是不可拷贝的(实际上,所有的标准流对象都是不可拷贝的),因而我们只能通过引用来访问一开始建立的那个流对象。但是,这种办法有一个问题,如果我们要从一个函数中返回一个流对象出来就不行了:

 

// how do we make this happen?

std::fstream createStream()
{ … }

 

当然,你可以用auto_ptr来解决这个问题,但这就使代码非常笨拙且难以维护。

 

但如果fstreammoveable的,以上代码就是可行的了。所谓“moveable”即是指(当源对象是临时对象时)在对象拷贝语法之下进行的实际动作是像auto_ptr那样的资源所有权转移:源对象被掏空,所有资源都被转移到目标对象中——好比一次搬家(move)。move操作之后,源对象虽然还有名有姓地存在着,但实际上其“实质”(内部拥有的资源)已经消失了,或者说,源对象从语意上已经消失了。

 

对于moveable但并非copyablefstream对象来说,当发生一次move时(比如在上面的代码中,当一个局部的fstream对象被movecreateStream()函数时),不会出现同一对象的两个副本,取而代之的是,move的源对象的身份(Identity)消失了,这个身份由返回的临时fstream对象重新持有。也就是说,fstream的唯一性(不可拷贝性——non-copyable)得到了尊重。

 

你可能会问,那么被搬空了的那个源对象如果再被使用的话岂不是会引发问题?没错。这就是为什么我们应该仅当需要且可以去move一个对象的时候去move它,比如在函数的最后一行(return)语句中将一个局部的vector对象move出来(return std::move(v)),由于这是最后一行语句,所以后面v不可能再被用到,对它来说所剩下的操作就是析构,因此被掏空从语意上是完全恰当的。

 

最初的例子——完美解决方案

在先前的那个例子中

 

vector<int> v = readFile();

 

有了move语意的话,readFile就可以简单的改成:

 

std::vector<int> readFile()

{

std::vector<int> retv;

… // fill retv

return std::move(retv); // move retv out

}

 

std::move以后再介绍。目前你只要知道,std::move就可以把retv掏空,即搬移出去,而搬家的最终目的地是v。这样的话,从内存分配的角度讲,只有retv中进行的内存分配,在从retv到返回的临时对象,再从后者到目的地v的“move”过程中,没有任何的内存分配(我是指vector内的缓冲区分配),取而代之的是,先是retv内的缓冲区被“转移”到返回值临时对象中,然后再从临时对象中转移到v中。相比于以前的两次拷贝而言,两次move操作节省了多少工作量呢?节省了两次new操作两次delete操作,还有两次O(n)的拷贝操作,这些操作整体的代价正比于retv这个vector的大小。难怪人们说临时对象效率问题是C++的肿瘤(wart)之一,难怪C++标准都要不惜代价允许(N)RVO

 

如何支持move语意

根据前面的介绍,你想必已经知道。实现move语意的最关键环节在于能够在编译期区分左值右值(也就是说识别出临时对象)。

 

现在,回忆一下,在文章的开头我曾经提到:

 

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

 

为什么这么说?

 

现行标准(C++03)下的方案

要想区分左值右值,只有通过重载:

 

void foo(X const&);

void foo(X&);

 

这样的重载显然是行不通的。因为X const&会把non-const临时对象一并吞掉。

 

这种做法的问题在于。X&是一个non-const引用,它只能接受non-const左值。然而,C++里面的值一共有四种组合:

 

        const    non-const

lvalue

rvalue

 

常量性(const-ness)与左值性(lvalue-ness)是正交的。

 

non-const引用只能绑定到其中的一个组合,即non-const lvalue。还剩下const左值,const右值,以及我们最关心的——non-const右值。而只有最后一种——non-const右值——才是可以move

 

剩下的问题便是如何设计重载函数来搞定const左值和const右值。使得最后只留下non-const右值。

 

所幸的是,我们可以借助强大的模板参数推导机制:

 

// catch non-const lvalues

void foo(X&);

 

// catch const lvalues and const rvalues

template<typename T>

void foo(T&, enable_if_same<T, const X>::type* = 0);

 

void foo( /* what goes here? */);

 

注意,第二个重载负责接受const左值和const右值。经过第一第二个foo重载之后剩下来的便是non-const rvalue了。

 

问题是,我们怎么捕获这些non-const rvalue呢?根据C++03const-const rvalue只能绑定到const引用。但如果我们用const引用的话,就会越俎代庖把const左右值一并接受了(因为在模板函数(第二个重载)和非模板函数(第三个重载)之间编译器总是会偏好非模板)。

 

那除了用const引用,难道还有什么办法来接受一个non-const rvalue吗?

 

有。

 

假设你的类型为X,那么只要在X里面加入一点料:

 

struct ref_x

{

ref_x(X* p) : p_(p) {}

X* p_;

};

 

struct X

{

// original stuff

 

// added stuff, for move semantic

operator ref_x()

{

return ref_x(this);

}

};

 

这样,我们的第三个重载函数便可以写成:

 

void foo(ref_x rx); // accept non-const temporaries only!

 

Bang! 我们成功地在C++03下识别出了moveablenon-const临时对象。不过前提是必须得在moveable的类型里加入一些东西。这也正是该方案的最大弊病——它是侵入式的(姑且不说它利用了语言的阴暗角落,并且带来了很大的编码复杂度)。

 

C++09的方案

实际上,刚才讲的这个利用重载的方案做成库便是Andreimojo框架。mojo框架固然精巧,但复杂性太大,使用成本太高,不够优雅直观。所以语言级别的支持看来是必然选择(后面你还会看到,为了支持move语意而引入的新的语言特性同时还支持了另一个广泛的问题——完美转发)。

 

C++03之所以让人费神就是因为它没有一个引用类型来绑定到右值,而是用const左值引用来替代,事实证明这个权宜之计并不是长远之道,时隔10年,终归还是要健全引用的左右值语意。

 

C++09加入一个新的引用类型——右值引用。右值引用的特点是优先绑定到右值。其语法是&&(注意,不读作“引用的引用”,读作“右值引用”)。有了右值引用,我们前面的方案便可以简单的修改为:

 

void foo(X const& x);

void foo(X&& x);

 

这样一来,左值以及const右值都被绑定到了第一个重载版本。剩下的non-const右值被绑定到第二个重载版本。

 

对于你的moveable的类型X,则是这样:

 

struct X

{

X();

X(X const& o); // copy constructor

X(X&& o); // move constructor

};

 

X source();

 

X x = source(); // #1

 

#1处,调用的将会是X::X(X&& o),即所谓的move constructor,因为source()返回的是一个临时对象(non-const右值),重载决议会选中move constructor

 

扩展阅读

由于本文的意图是一个指南,因此关于move语意的其它林林种种的细节可参见下面列的参考文献。

 

[1] Move Constructor

[2] A Proposal to Add Move Semantics Support to the C++ Language (a.k.a. N1377)

[3] Yasli::vector is on the Move

[4] Clarification of Initialization of Class Objects by rvalues (a.k.a. N1610)

[5] A Brief Intro to rvalue reference (a.k.a. N2027)

[6] A Proposal to Add Rvalue Reference to the C++ Language - Proposed Wording (a.k.a. N2118)

[7] State of C++ Evolution (Toronto 2007 Meeting) (a.k.a. 2291)

 

下篇预告

下篇是关于完美转发的。

 

目录(展开C++0x漫谈》系列文章)

 

发表于 @ 2007年07月10日 16:09:00|评论(loading...)

新一篇: 《C++0x漫谈》系列之:右值引用(或“move语意与完美转发”)(下)  | 旧一篇: 我不想与我不能

用户操作
[即时聊天] [发私信] [加为好友]
刘未鹏
订阅我的博客
XML聚合  FeedSky
订阅到鲜果
订阅到Google
订阅到抓虾
刘未鹏的公告
除非特别声明,本站采用Creative Commons License许可。转载请保留作者、出处。非商业。

重要公告

本博客已经迁移至 http://mindhacks.cn ,此处保留作为镜像,但不保证一定同步更新所有内容。原订阅 http://blog.csdn.net/pongba/rss.aspx (原始 Feed) 的朋友请转为订阅永久 feed : http://mindhacks.cn/feed/

关于

我经常在 @TopLanguage | @Twitter | @Douban

《C++的罗浮宫》5年选集

——知识分享是最大的复用

下载地址:csdn资源频道|mediafire

讨论问题请到TopLanguage综合技术讨论组

TopLanguage

精彩言论@TopLanguage


pongba的共享阅读@Delicious


pongba@Twitter


pongba在读@豆瓣


gtalk/msn(邮件请发送到gmail邮箱)

pongba@gmail.com
pp_liu@msn.com

搜索(不要回车,点击Go)


pongba翻译的





这个Blog上都写了哪些东东

文章分类
收藏
C++
Andrei Alexandrescu
Andrew Lumsdaine
Bjarne Stroustrup
boost
C++ Standard Commitee
Doug Gregor
Hans J. Boehm
Jaakko Jarvi
Jeremy G. Siek
Matthew Wilson
newsgroups
boost.Developer
boost.User
comp.lang.c++.moderated
comp.std.c++
TopLanguage
Open Source
Ant
codeplex
Danga
Google AJAX Search API
Google Code Prettify
Google Web Toolkit
Hadoop
MS shared source initiative
notepad++
STLSoft
不认识的朋友们
Delphij
fatalerror99
flow with the life
Glacier
jimaxsoft
lifesinger@淘宝UED
Mr. 6
realazy
Robbin
SpiritEpic
TK
wuyizi
Yelz
丁丁虫
付翀
冰云
刘慈欣
卢昌海
吴欣安(atppp)
周爱民
和菜头
姬十三
守望轩
小花@BlogBus
林达华
浦宇平
白鸦
程化
罗浩|Startup Game
阮一峰
霍炬
飞之鸿
高远
鲍盛
机器学习/数据挖掘/信息检索/自然语言处理/认知科学/人工智能
AAAI
Apex
arXiv
Charles Kemp
Christopher Bishop
Christopher Manning
Cognitive Daily
Dan Jurafsky
David MacKay
ECML PKDD
Geoffrey Hinton
Herbert Simon
ICML
IJCAI
Jeff Hawkins
Jiawei Han
JMLR
Josh Tenenbaum
Larry Wasserman
Lucene
Marvin Minsky
MIT AI Lab
MIT Computational Cognitive Science Group
Mitchell Marcus
ML
NetLab
NIPS
Peter Norvig
Stanford AI Lab
Stanford NLP Lab
Stephen Boyd
Tom Mitchell
Trends in Cognitive Science
Vladimir Vapnik
Weka
Zhihua Zhou
技术
Coding Horror
High Scalability
Reddit
Stack Overflow
Steve Yegge
代码发芽网
淘宝UED团队
淘宝数据仓库团队
玩聚网
移山之道
其它
Gigapedia
Scientific American
Scientific American Mind
科学松鼠会
科幻世界
认识的朋友们
alai
chenyufei
dd
DreamHead
Googol
Jawley
Joyfire
littlestone
lxwde
Matrix67
realfun
RiceBall
roofalison
soloist
Tinyfool
windstorm
YongSun
书剑
云风
余晟
元凯宁
冯大辉(Fenng)
刘新宇
刘江@图灵
史晓明
吴新雨
周星星
周筠@博文视点
孟岩
张志强|阅微堂
张振
徐宥|4G Spaces
方舟@博文视点
曾登高
李笑来|Pure Pleasure
杨军
杨文博
熊节
王信文
王康生
苏杰@阿里巴巴
范怀宇
荣耀
莫华枫
蒋涛
袁泳(g9)|负暄琐话
许式伟
谢东升
谷文栋|Beyond Search
邹欣@MSRA
郑昀
阿朱
陈冀康@华章
陈怀兴
鲍志云
存档
Csdn Blog version 3.1a
Copyright © 刘未鹏