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

来源:互联网 发布:电视直播回放软件 编辑:程序博客网 时间:2024/06/16 04:53

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++中的引用类型在左值右值方面的缺陷。从库设计者的角度讲,它给库设计者又带来了一把利器。从库使用者的角度讲,不动一兵一卒便可以获得“免费的”效率提升

 

 

完美转发

 

完美转发问题——不完美解决方案——模板参数推导规则——完美转发

 

动机

关于“完美转发”这个特性,其实提案N1385已经讲得非常清楚了,诸位可以直接去看N1385,如果实在还是觉得迷糊就再回来听我唠叨吧:-)

 

在泛型编码中经常出现的一个问题是(这个问题在实际中出现的场景很多,我们留到文章末尾再提,目前我们将这个特定的问题先提取孤立出来考虑):

 

如何将一组参数原封不动地转发给另一个函数

 

注意,这里所谓“原封不动”就是指,如果参数是左值,那么转发给的那个函数也要接受到一个左值,如果参数是右值,那么后者要接受到一个右值;同理,如果参数是const的,那么转发给的那个函数也要接受到一个const的值,如果是non-const的,那么后者也要接受到一个non-const的值。

 

总之一句话:

 

保持参数的左值/右值、const/non-const属性不变

 

听上去很简单吗?不妨试试看。

 

(不完美的)解决方案

假设我们要写一个泛型转发函数ff要将它的参数原封不动地转发给g(不管g的参数类型是什么):

 

template

void f(/*what goes here?*/ t)

{

g(t);

}

 

上面的代码中,f的参数t的类型是什么?TT&const T&

 

我们一个个来分析。

 

Value

如果t的类型是T,即:

 

// take #1

template

void f(T t)

{

g(t);

}

 

那么很显然,不能满足如下情况:

 

void g(int& i) { ++i; }

 

int myInt = 0;

 

f(myInt); // error, the value g() has incremented is a local value(a.k.a. f’s argument ‘t’)

 

即,不能将左值转发为左值。

 

Const&

如果t的类型为const T&,即:

 

// take #2

template

void f(const T& t)

{

g(t);

}

 

则刚才的情况还是不能满足。因为g接受的参数类型为non-const引用。

 

Non-const&

那如果t的类型是T&呢?

 

// take #3

template

void f(T& t)

{

g(t);

}

 

我们惊讶地发现,这时,如果参数是左值,那么不管是const左值还是non-const左值,f都能正确转发,因为对于const左值,T将会被推导为const UU为参数的实际类型)。并且,对于const右值,f也能正确转发(因为const引用能够绑定到右值)。只有对non-const右值不能完美转发(因为这时T&会被推导为non-const引用,而后者不能绑定到右值)。

 

即四种情况里面有三种满足了,只有以下这种情况失败:

 

void g(const int& i);

 

int source();

 

f(source()); // error

 

如果f是完美转发的话,那么f(source())应该完全等价于g(source()),后者应该通过编译,因为g是用const引用来接受参数的,后者在面对一个临时的int变量的时候应该完全能够绑定。

 

而实际上以上代码却会编译失败,因为f的参数是T&,当面对一个non-constint型右值(source()的返回值)时,会被推导为int&,而non-const引用不能绑定到右值。

 

好,现在的问题就变成,如何使得non-const右值也被正确转发,用T&f的参数类型是行不通的,唯一能够正确转发non-const右值的办法是用const T&来接受它,但前面又说过,用const T&行不通,因为const T&不能正确转发non-const左值。

 

Const& + non-const&

那两个加起来如何?

 

template

void f(T& t)

{

g(t);

}

 

template

void f(const T& t)

{

g(t);

}

 

一次重载。我们来分析一下。

 

对于non-const左值,重载决议会选中T&,因为绑定到non-const引用显然优于绑定到const引用(const T&)。

 

对于const左值,重载决议会选中const T&,因为显然这是个更specialized的版本。

 

对于non-const右值,T&根本就行不通,因此显然选中const T&

 

对于const右值,选中const T&,原因同第二种情况。

 

可见,这种方案完全保留了参数的左右值和const/non-const属性。

 

值得注意的是,对于右值来说,由于右值只能绑定到const引用,所以虽然const T&并非“(non-)const右值”的实际类型,但由于C++03只能用const T&来表达对右值的引用,所以这种情况仍然是完美转发。

 

组合爆炸

你可能会觉得上面的这个方案(const& + non-const&)已经是完美解决方案了。没错,对于单参的函数来说,这的确是完美方案了。

 

但是如果要转发两个或两个以上的参数呢?

 

对于每个参数,都有const T&T&这两种情况,为了能够正确转发所有组合,必须要2N次方个重载

 

比如两个参数的:

 

template

void f(T1& t1, T2& t2) { g(t1, t2); }

 

template

void f(const T1& t1, T2& t2) { g(t1, t2); }

 

template

void f(T1& t1, const T2& t2) { g(t1, t2); }

 

template

void f(const T1& t1, const T2& t2) { g(t1, t2); }

 

(完美的)解决方案

理想情况下,我们想要:

 

template

void f(/*what goes here?*/ t1, /**/ t2, … )

{

  g(t1, t2);

}

 

填空处应该填入一些东西,使得当t1对应的实参是non-const/const的左/右值时,t1的类型也得是non-const/const的左/右值。目前的C++03中,non-const/const属性已经能够被正确推导出来(通过模板参数推导),但左右值属性还不能。

 

明确地说,其实问题只有一个:

 

对于non-const右值来说,模板参数推导机制不能正确地根据其右值属性确定T&的类型(也就是说,T&会被编译器不知好歹地推导为左值引用)。

 

修改T&non-const右值的推导规则是可行的,比如对这种情况:

 

template

void f(T& t);

 

f(1);

 

规定T&推导为const int&

 

但这显然会破坏既有代码。

 

很巧的是,右值引用能够拯救世界,右值引用的好处就是,它是一种新的引用类型,所以对于它的规则可以任意制定而不会损害既有代码,设想:

 

template

void f(T&& t){ g(t); }

 

我们规定:

 

如果实参类型为右值,那么T&&就被推导为右值引用。

如果实参类型为左值,那么T&&就被推导为左值引用。

 

Bingo!问题解决!为什么?请允许我解释。

 

f(1); // T&& 被推导为 int&&,没问题,右值引用绑定到右值。

f(i); // T&& 被推导为 int&,没问题,通过左值引用完美转发左值。

 

等等,真没问题吗?对于f(1)的情况,t的类型虽然为int&&(右值引用),但那是否就意味着t本身是右值呢?既然t已经是具名(named)变量了,因此t就有被多次move(关于move语意参考上一篇文章)的危险,如:

 

void dangerous(C&& c)

{

C c1(c); // would move c to c1 should we allow treating c as a rvalue

c.f(); // disaster

}

 

在以上代码中,如果c这个具名变量被当成右值的话,就有可能先被move掉,然后又被悄无声息的非法使用(比如再move一次),编译器可不会提醒你。这个邪恶的漏洞是因为c是有名字的,因此可以被多次使用。

 

解决方案是把具名的右值引用作为左值看待

 

但这就使我们刚才的如意算盘落空了,既然具名的右值引用是左值的话,那么f(1)就不能保持1的右值属性进行转发了,因为f的形参t的类型(T&&)虽然被推导为右值引用(int&&),但t却是一个左值表达式,也就是说f(1)把一个右值转发成了左值。

 

最终方案

通过严格修订对于T&&的模板参数推导原则,以上问题可以解决。

 

修订后的模板参数推导规则为:

 

如果实参是左值,那么T就被推导为U&(其中U为实参的类型),于是T&& = U& &&,而U& &&则退化为U&(理解为:左值引用的右值引用仍然是左值引用)。

 

如果实参是右值,那么T就被推导为U,于是T&& = U&&(右值引用)。

 

如此一来就可以这样解决问题:

 

template

void f(T&& t)

{

  g(static_cast(t));

}

 

想想看,如果实参为左值,那么T被推导为U&T&&U& &&,也就是U&,于是static_cast也就是static_cast,转发为左值。

 

如果实参为右值,那么T被推导为UT&&U&&static_cast也就是static_cast,不像t这个具名的右值引用被看作左值那样,static_cast(t)这个表达式由于产生了一个新的无名(unnamed)值,因而是被看作右值的。于是右值被转发为了右值。

 

扩展阅读

[1] Rvalue.Reference.Proposed.Wording(rev#3) (a.k.a. N2118)

[2] Impact of the rvalue reference on the Standard Library (a.k.a. N1771)

 

下篇预告

下篇会写variadic templates。然后介绍tr1::tuple的新版实现。

 

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

 




原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 收件人号码写错快递柜已签收怎么办 医院名字写错了怎么办保险报销 电脑在使用中出现了英文字慕怎么办 下雨天了怎么办我好想你是什么歌 安卓手机不支持微信运动怎么办 装系统时无法跳过密匙怎么办 在msdn里下的系统没有网怎么办 w7电脑更新后系统没法激活怎么办 手机使用加速器后网速变卡怎么办 奥特曼ol分解了迪迦石像怎么办 左右棋牌游戏兑换总说系统护怎么办 四季海棠扦插以后黄叶卷叶怎么办 竹节海棠浇水多了叶子蔫了怎么办 社保停缴了里面的钱怎么办 王者荣耀英雄释放技能有延迟怎么办 买的桑拿木板颜色太深了怎么办 万一填写了奔跑吧诈骗信息该怎么办 深圳限行时段堵在路上怎么办 开车堵在路上到了限行时间怎么办 兄妹之间都不想照顾母亲我该怎么办 小孩扁体发炎睡觉呼吸声沉重怎么办 客所思pk3老驱动有杂音怎么办 手机打不开解压包密码怎么办 在香港专柜买东西柜员少给货怎么办 恶魔猎手第二神器任务没选择怎么办 电脑放久了开不了机怎么办 你在主持时说错话了怎么办 1、你在主持时说错话了怎么办? 领导让你替他参加重要会议怎么办 在备孕期老公照了片怎么办 和混混打架后被混混纠缠怎么办 开了一年的瑜伽馆想关掉会员怎么办 广东工厂宿舍里面床板有臭虫怎么办 胃癌手术后12天引流液多怎么办 八个月了胎儿头还在上面怎么办 喂奶一个月后奶头又裂开了怎么办 孩子吃奶吃的奶头裂开了怎么办 很想打坐久就是受不了腿疼怎么办 宝宝含乳一直纠正不过来怎么办 婴儿衘乳不正确只吸乳头怎么办 怀孕7个月了晚上睡不着怎么办