《C++0x漫谈》系列之:右值引用
或“move语意与完美转发”(下)
By 刘未鹏(pongba)
刘言|C++的罗浮宫(http://blog.csdn.net/pongba)
《C++0x漫谈》系列导言
这个系列其实早就想写了,断断续续关注C++0x也大约有两年余了,其间看着各个重要proposals一路review过来:rvalue-references、concepts、memory-model、variadic-templates、template-aliases、auto/decltype、GC、initializer-lists…
总的来说C++09跟C++98相比的变化是极其重大的。这个变化体现在三个方面,一个是形式上的变化,即在编码形式层面的支持,也就是对应我们所谓的编程范式(paradigm)。C++09不会引入新的编程范式,但在对泛型编程(GP)这个范式的支持上会得到质的提高:concepts、variadic-templates、auto/decltype、template-aliases、initializer-lists皆属于这类特性。另一个是内在的变化,即并非代码组织表达方面的,memory-model、GC属于这一类。最后一个是既有形式又有内在的,r-value references属于这类。
这个系列如果能够写下去,会陆续将C++09的新特性介绍出来。鉴于已经有许多牛人写了很多很好的tutor(这里,这里,还有C++标准主页上的一些introductive的proposals,如这里,此外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属性不变
听上去很简单吗?不妨试试看。
(不完美的)解决方案
假设我们要写一个泛型转发函数f,f要将它的参数原封不动地转发给g(不管g的参数类型是什么):
template<typename T>
void f(/*what goes here?*/ t)
{
g(t);
}
上面的代码中,f的参数t的类型是什么?T?T&?const T&?
我们一个个来分析。
Value
如果t的类型是T,即:
// take #1
template<typename T>
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<typename T>
void f(const T& t)
{
g(t);
}
则刚才的情况还是不能满足。因为g接受的参数类型为non-const引用。
Non-const&
那如果t的类型是T&呢?
// take #3
template<typename T>
void f(T& t)
{
g(t);
}
我们惊讶地发现,这时,如果参数是左值,那么不管是const左值还是non-const左值,f都能正确转发,因为对于const左值,T将会被推导为const U(U为参数的实际类型)。并且,对于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-const的int型右值(source()的返回值)时,会被推导为int&,而non-const引用不能绑定到右值。
好,现在的问题就变成,如何使得non-const右值也被正确转发,用T&作f的参数类型是行不通的,唯一能够正确转发non-const右值的办法是用const T&来接受它,但前面又说过,用const T&行不通,因为const T&不能正确转发non-const左值。
Const& + non-const&
那两个加起来如何?
template<typename T>
void f(T& t)
{
g(t);
}
template<typename T>
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&这两种情况,为了能够正确转发所有组合,必须要2的N次方个重载
比如两个参数的:
template<typename T1, typename T2>
void f(T1& t1, T2& t2) { g(t1, t2); }
template<typename T1, typename T2>
void f(const T1& t1, T2& t2) { g(t1, t2); }
template<typename T1, typename T2>
void f(T1& t1, const T2& t2) { g(t1, t2); }
template<typename T1, typename T2>
void f(const T1& t1, const T2& t2) { g(t1, t2); }
(完美的)解决方案
理想情况下,我们想要:
template<typename T1, typename T2, … >
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<typename T>
void f(T& t);
f(1);
规定T&推导为const int&。
但这显然会破坏既有代码。
很巧的是,右值引用能够拯救世界,右值引用的好处就是,它是一种新的引用类型,所以对于它的规则可以任意制定而不会损害既有代码,设想:
template<typename T >
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<typename T>
void f(T&& t)
{
g(static_cast<T&&>(t));
}
想想看,如果实参为左值,那么T被推导为U&,T&&为U& &&,也就是U&,于是static_cast<T&&>也就是static_cast<U&>,转发为左值。
如果实参为右值,那么T被推导为U,T&&为U&&,static_cast<T&&>也就是static_cast<U&&>,不像t这个具名的右值引用被看作左值那样,static_cast<U&&>(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漫谈》系列文章)
分享到:
相关推荐
右值引用(及其支持的Move语意和完美转发)是C++0x将要加入的最重大语言特性之一。从实践角度讲,它能够完美解决C++中长久以来为人所诟病的临时对象效率问题。从语言本身讲,它健全了C++中的引用类型在左值右值方面...
// a是左值 getTemp()的返回值是右值(临时变量)左值引用、右值引用c++98中的引用很常见了,就是给变量取了个别名,在c++11中,因为增加了右值引
为了解决移动语义及完美转发问题,C++11标准引入了右值引用(rvalue reference)这一重要的新概念。右值引用采用T&&这一语法形式,比传统的引用T&(如今被称作左值引用 lvalue reference)多一个&。 如果把经由T&&...
C++11的新特性。尤其是lamda表达式,使得C++灵活了很多
C++11:右值引用-附件资源
第一次接触c++move操作就懵逼了,一直想探个究竟,但是右值以及右值引用思考了好长时间,就是不得要领,今天终于有所收获,写下第一篇博客,一方面为了帮助一些刚入门的朋友,另一方面也是帮助自己今后复习。...
对于c++11来说移动语义是一个重要的概念,一直以来我对这个概念都似懂非懂。最近翻翻资料感觉突然开窍,因此顺便记录下C++11中的右值引用、转移语义和完美转发,方便大家查阅参考。
主要介绍了C++11 模板参数的“右值引用”是转发引用吗,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
这一系列文章来自Visual C++ Team Blog,介绍Microsoft Visual Studio 2010 中支持的C++0x特性,目前有三部分。 Part 1:介绍了Lambdas,auto,以及 static_assert; Part 2:介绍了右值引用(Rvalue References)...
右值引用(及其支持的Move语意和完美转发)是C++0x将要加入的最重大语言特性之一,这点从该特性的提案在C++ – State of the Evolution列表上高居榜首也可以看得出来。 从实践角度讲,它能够完美解决C++中长久以来...
在c++11中,支持右值引用,右值引用的用处之一是移动语义,对象的资源所有权发生转移,在c++11之前,移动语义的缺失是c++饱受诟病的问题之一. 什么是左值?什么是右值? 凡有名者,皆为左值.左值对应变量的存储位置...
左值:可以取地址的,有名字的,临时的右值:不能取地址的,没有名字的,临时的举个栗: int a = b + c ,a 就是左值,其变量名为 a ,通过 &a 可
这一系列文章介绍Microsoft Visual Studio 2010 中支持的C++ 0x特性。 Part 1 :介绍了Lambdas, 赋予新意义的auto,以及 static_assert; Part 2( 1 , 2 ):介绍了右值引用(Rvalue References); Part 3:介绍了...
std::move函数可以以非常简单的方式将左值引用转换为右值引用。(左值、左值引用、右值、右值引用 参见:http://www.cnblogs.com/SZxiaochun/p/8017475.html) 通过std::move,可以避免不必要的拷贝操作。 std::move...
C++11 引入了 std::move 语义、右值引用、移动构造和完美转发这些特性。由于这部分篇幅比较长,分为3篇来进行阐述。 在了解这些特性之前,我们先来引入一些问题。 一、问题导入 函数返回值是传值的时候发生几次...
1、右值引用引入的背景 临时对象的产生和拷贝所带来的效率折损,一直是C++所为人诟病的问题。但是C++标准允许编译器对于临时对象的产生具有完全的自由度,从而发展出了CopyElision、RVO(包括NRVO)等编译器优化技术...