右值引用:模板参数推导和引用折叠

来源:互联网 发布:iphone手机称重软件 编辑:程序博客网 时间:2024/04/19 19:47

C++0x和C++11为了避免不必要的复制构造以提高程序效率【转移语义】和很容易地实现高度通用的模板代码【完美转发】,引入了右值引用。要理解右值引用不仅要理解左值和右值的区别,还需要知道下面的规则,才能理解std::move和std::forward的实现原理。【英文原文地址】

右值引用:函数模板参数推导和引用折叠
右值引用和模板以一种特别的方式相互作用。

<span style="font-size:18px;">#include <iostream>#include <string>using namespace std;template <typename T> struct Name;template <> struct Name<string> {static const char * get() {return "string";}};template <> struct Name<const string> {static const char * get() {return "const string";}};template <> struct Name<string&> {static const char * get() {return "string&";}};template <> struct Name<const string&> {static const char * get() {return "const string&";}};template <> struct Name<string&&> {static const char * get() {return "string&&";}};template <> struct Name<const string&&> {static const char * get() {return "const string&&";}};template <typename T> void quark(T&& t) {cout << "t: " << t << endl;cout << "T: " << Name<T>::get() << endl;cout << "T&&: " << Name<T&&>::get() << endl;cout << endl;}string strange() {return "strange()";}const string charm() {return "charm()";}void main() {string up("up");const string down("down");quark(up);quark(down);quark(strange());quark(charm());}</span>
输出结果:

<span style="font-size:18px;">t: upT: string&T&&: string&</span>
<span style="font-size:18px;">t: downT: const string&T&&: const string&</span>
<span style="font-size:18px;">t: strange()T: stringT&&: string&&</span>
<span style="font-size:18px;">t: charm()T: const stringT&&: const string&&</span>

这里Name的显式特化帮助我们打印出类型的名字。
当我们调用quark(up),编译器先进行模板类型参数的推导。quark是一个函数模板,其模板参数是T,但是我们调用它时并没有显式的给出类型参数(像quark<X>(up)),而是通过比较函数的形参Type&&和实参(一个string类型的左值)来推导出模板的类型参数。
C++0x 会转换函数实参的类型和形参的类型,然后再把它们匹配在一起。
首先,编译器转换函数实参类型。一条特殊的规则(N2798 14.8.2.1 [temp.deduct.call]/3)被激活了:当一个函数形参类型是T&&(T是一个模板参数),函数实参又是一个A类型的左值,那么使用A&来进行模板参数推导。(这条特殊的规则不适用于函数实参为T&或是const T&的情况,它们会按照C++98/03规则推导。此特殊规则也不适用于const T&&。)在quark(up)这种情况,适用特殊规则,实参类型被转换为string&。
接着,编译器转换函数形参类型,C++98/03和C++0x都会忽略引用(C++0x即忽略左值引用,又忽略右值引用),在所有四次调用时,都意味着我们将T&&转换成为了T。
因此,我们推导出T为函数实参类型。这就是为啥quark(up)输出了“T::string&”,quark(down)输出了“T::const string&”,up和down都是左值,因此它们激活了那条特殊规则。strange()和charm()是右值,因此它们使用正常的规则,这就是为啥quark(strange())输出“T::string”,quark(charm())输出“T::const string”了。
模板参数推导之后,编译器开始替换操作。编译器把每个出现的T都替换成推导出的类型,在quark(strange())中,T是string,因此T&&是string&&。类似的,在quark(charm())中,T是const string,因此T&&是const string&&。但是,quark(up)和quark(down)激活了另一条特殊规则。
在quark(up)中,T是string&,T&&的替换操作导致了结果string& &&。在C++0x中引用的引用将会退化,并且引用退化的规则是“左值的引用具有传染性”,X& &,X& &&,和X&& &,都退化为X&,只有X&& &&退化为了X&&。因此,string& &&退化到string&。在模板函数中,那些看起来像右值引用的参数,其实并不一定是。quark(up)实例化为了quark<string&>()。在这个实例中,参数T&&变为了string&。我们通过Name<T&&>::get()已经观察到了这一点。类似的,quark(down)实例化为了quark<const string&>(),参数T&&变为了const string&。在C++98/03中,你可能已经习惯了常量性隐藏于模板参数中了(一个接受参数T&的函数模板可以使用const Foo对象去调用;使T&看起来变为了const Foo&),在C++0x中,左值性也可以隐藏在函数模板参数中。
好的,那么我们要问这两条特殊规则给我们带来了什么?在quark()中,T&&有着和实参同样的左右值和常量属性,因此可以用右值引用这种保留实参左右值和常量性的特点实现完美转发。 完美转发:std::forward()和std::identity如何工作
让我们再次看一下outer():

<span style="font-size:18px;">template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) {inner(Forward<T1>(t1), Forward<T2>(t2));}</span>

现在我们明白了为什么outer()接受参数类型为T1&&和T2&&了。这样outer()实参的所有信息都会被保留。但是为什么它要调用Forward<T1>()和Forward<T2>()?回想一下,所有具名左值引用和具名右值引用都是左值。如果outer()调用inner(t1, t2),那么inner()接收到的参数将总是左值,转发就不完美了。
幸运的是,匿名的左值引用是左值,而匿名的右值引用是右值。因此,为了将t1和t2转发给inner(),我们需要使用辅助函数保存它们的类型信息但是移除它们的名字。这就是std::forward()的功能:
<span style="font-size:18px;">template <typename T> struct Identity {typedef T type;};template <typename T> T&& Forward(typename Identity<T>::type&& t) {return t;}</span>

当我们调用Forward<T1>(t1)时,Identity并没有修改T1的类型(我们一会儿将看到他干了什么)。因此Forward<T1>()接受参数为T1&&并且返回T1&&。这保证了t1的类型无改变(不管它是什么:string&,const string&,string&&,const string&&)而又移除了它的名字。inner()将收到Forward<T1>(t1),它和t1具有同样的左右值/常量属性。这就是完美转发的工作原理。
你可能会问如果把Forward<T1>(t1)写成Forward<T1&&>(t1)的话会发生什么(这是个经常出现的错误,因为outer的参数就是T1&&)。幸运的是,这样不会导致什么坏的结果。因为Forward<T1&&>()将接受和返回T1&& &&类型,它会退化成T1&&。因此Forward<T1>(t1)和Forward<T1&&>(t1)是一样的,但是前面的形式更短,因此更加流行。
Identity干了什么?为什么下面的形式无法工作呢?
<span style="font-size:18px;">template <typename T> T&& Forward(T&& t) { // BROKENreturn t;}</span>

如果像上面这样实现Forward(),那么就可以不显示的指定模板参数而调用它,模板类型参数推导机制就会插进来,而我们之前已经看到了在T&&上面推导会发生:当调用它的实参是左值的时候,T&&将会变成左值。而我们要实现的是在outer()中,具名的t1和t2是左值的时候,我们也要将其改为右值转发。使用上面的BROKEN实现,无法完成这个功能,T&&有可能被编译器推导为左值。因此我们使用Identity来阻止编译器的模板参数推导机制的介入。经常使用模板的程序员应该对这个很熟悉了,因为这个在C++98/03和C++0x中都完成同样的工作:typename Identity<T>::type中的那对冒号就像块铅板一样,编译器的模板类型推导无法穿越到它的左边。(解释其原理又是另外的话题了)
move语义:std::move()是如何工作的
现在我们已经了解了模版类型推导中的特殊规则,以及引用退化,让我们再看一下std::move():
<span style="font-size:18px;">template <typename T> struct RemoveReference {typedef T type;};template <typename T> struct RemoveReference<T&> {typedef T type;};template <typename T> struct RemoveReference<T&&> {typedef T type;};template <typename T> typename RemoveReference<T>::type&& Move(T&& t) {return t;}</span>

RemoveReference的实现机制和C++0x头文件<type_traits>中的std::remove_reference一样。例如:RemoveReference<string>::type,RemoveReference<string&>::type,和RemoveReference<string&&>::type都是string。
同样的,Move()和C++0x头文件<utility>的实现机制一样。
当Move()被一个左值string调用,T被推断为string&,因此Move()接受到string&类型的参数(引用退化过以后),经过RemoveReference,返回值为string&&。
当Move()被一个左值const string调用,T被推断为const string&,因此Move()接受到const string&类型的参数(引用退化过以后),经过RemoveReference,返回值为const string&&。
当Move()被一个右值string调用,T被推断为string,因此Move()接受到string&&类型的参数,经过RemoveReference,返回值为string&&。
当Move()被一个右值const string调用,T被推断为const string,因此Move()接受到const string&&类型的参数,经过RemoveReference,返回值为const string&&。
这就是std::move()保持类型的常量属性的同时,将左值转换为右值返回的原理。
回顾
如果你想了解更多的右值引用的信息,你可以阅读它们的提案。要注意的是现在的情况可能已经跟提案不太一样了,右值引用已经被纳入了C++0x Working Paper,并得到了持续的改进。提案中有的部分已经过时了,有的不再正确,或者没有被C++0x标准采纳。但它仍然具有很大的参考价值。
N1377,N1385,和N1690是右值引用主要的草案。N2118包含了草案的被纳入C++0x Working Paper之前的最终版本。N1784,N1821,N2377,和N2439记录了将move语义扩展到*this的进化过程,它已经成为了C++0x标准,但VC10尚未实现它。
展望
N2812“右值引用的一个安全问题(以及如何解决它)”提出了对初始化规则的修改:禁止右值引用绑定到左值上。这不会影响到move语义和完美转发,所以不会导致你刚学到的新技术的失效(但是它会导致std::move()和std::forward()实现方式的改变)
Stephan T. Lavavej
Visual C++ Libraries Developer
 
博主后记
我们知道,左值是不可以绑定到右值引用的。但是看完上面的解释以后,我们知道函数模板和普通函数是不一样的。
所以下面的代码test1(i)不能通过编译,而test2(i)能够通过编译。
<span style="font-size:18px;">#include <utility>void test1(int &&i){}template<typename T>void test2(T &&i){}void main(){ int i=3; test1(i);//error C2664: 'test1' : cannot convert parameter 1 from 'int' to 'int &&' test2(i);//OK}</span>

0 0
原创粉丝点击