一、理解引用折叠
(一)引用折叠
1. 在C++中,“引用的引用”是非法的。像auto& &rx = x;(注意两个&之间有空格)这种直接定义引用的引用是不合法的,但是编译器在通过类型别名或模板参数推导等语境中,会间接定义出“引用的引用”,这时引用会形成“折叠”。
2. 引用折叠会发生在模板实例化、auto类型推导、创建和运用typedef和别名声明、以及decltype语境中。
(二)引用折叠规则
1. 两条规则
(1)所有右值引用折叠到右值引用上仍然是一个右值引用。如X&& &&折叠为X&&。
(2)所有的其他引用类型之间的折叠都将变成左值引用。如X& &, X& &&, X&& &折叠为X&。可见左值引用会传染,沾上一个左值引用就变左值引用了。根本原因:在一处声明为左值,就说明该对象为持久对象,编译器就必须保证此对象可靠(左值)。
2. 利用引用折叠进行万能引用初始化类型推导
(1)当万能引用(T&& param)绑定到左值时,由于万能引用也是一个引用,而左值只能绑定到左值引用。因此,T会被推导为T&类型。从而param的类型为T& &&,引用折叠后的类型为T&。
(2)当万能引用(T&& param)绑定到右值时,同理,右值只能绑定到右值引用上,故T会被推导为T类型。从而param的类型就是T&&(右值引用)。
【编程实验】引用折叠
#include <iostream> using namespace std; class Widget{}; template<typename T> void func(T&& param){} //Widget工厂函数 Widget widgetFactory() { return Widget(); } //类型别名 template<typename T> class Foo { public: typedef T&& RvalueRefToT; }; int main() { int x = 0; int& rx = x; //auto& & r = x; //error,声明“引用的引用”是非法的! //1. 引用折叠发生的语境1——模板实例化 Widget w1; func(w1); //w1为左值,T被推导为Widget&。代入得void func(Widget& && param); //引用折叠后得void func(Widget& param) func(widgetFactory()); //传入右值,T被推导为Widget,代入得void func(Widget&& param) //注意这里没有发生引用的折叠。 //2. 引用折叠发生的语境2——auto类型推导 auto&& w2 = w1; //w1为左值auto被推导为Widget&,代入得Widget& && w2,折叠后为Widget& w2 auto&& w3 = widgetFactory(); //函数返回Widget,为右值,auto被推导为Widget,代入得Widget w3 //3. 引用折叠发生的语境3——tyedef和using Foo<int&> f1; //T被推导为int&,代入得typedef int& && RvalueRefToT;折叠后为typedef int& RvalueRefToT //4. 引用折叠发生的语境3——decltype decltype(x)&& var1 = 10; //由于x为int类型,代入得int&& rx。 decltype(rx) && var2 = x; //由于rx为int&类型,代入得int& && var2,折叠后得int& var2 return 0; }
二、完美转发
(一)std::forward原型
//左值版本 template<typename T> T&& forward(typename remove_reference<T>::type& param) { return static_cast<T&&>(param); //可能会发生引用折叠! } //右值版本 template<typename T> T&& forward(typename remove_reference<T>::type&& param) { return static_cast<T&&>(param); }
(二)分析std::forward<T>实现条件转发的原理(以转发Widget类对象为例)
1. 当传递给func函数的实参类型为左值Widget时,T被推导为Widget&类别。然后forward会实例化为std::forward<Widget&>,并返回Widget&(左值引用,根据定义是个左值!)
2. 而当传递给func函数的实参类型为右值Widget时,T被推导为Widget。然后forward被实例化为std::forward<Widget>,并返回Widget&&(注意,匿名的右值引用是个右值!)
3. 可见,std::forward会根据传递给func函数实参(注意,不是形参)的左/右值类型进行转发。当传给func函数左值实参时,forward返回左值引用,并将该左值转发给process。而当传入func的实参为右值时,forward返回右值引用,并将该右值转发给process函数。
【编程实验】不完美转发和完美转发
#include <iostream> using namespace std; void print(const int& t) //左值版本 { cout <<"void print(const int& t)" << endl; } void print(int&& t) //右值版本 { cout << "void print(int&& t)" << endl; } template<typename T> void testForward(T&& param) { //不完美转发 print(param); //param为形参,是左值。调用void print(const int& t) print(std::move(param)); //转为右值。调用void print(int&& t) //完美转发 print(std::forward<T>(param)); //只有这里才会根据传入param的实参类型的左右值进转发 } int main() { cout <<"-------------testForward(1)-------------" <<endl; testForward(1); //传入右值 cout <<"-------------testForward(x)-------------" << endl; int x = 0; testForward(x); //传入左值 return 0; } /*输出结果 -------------testForward(1)------------- void print(const int& t) void print(int&& t) void print(int&& t) //完美转发,这里转入的1为右值,调用右值版本的print -------------testForward(x)------------- void print(const int& t) void print(int&& t) void print(const int& t) //完美转发,这里转入的x为左值,调用左值版本的print */
三、std::move和std::forward
(一)两者比较
1. move和forward都是仅仅执行强制类型转换的函数。std::move无条件地将实参强制转换成右值。而std::forward则仅在某个特定条件满足时(传入func的实参是右值时)才执行强制转换。
2. std::move并不进行任何移动,std::forward也不进行任何转发。这两者在运行期都无所作为。它们不会生成任何可执行代码,连一个字节都不会生成。
(二)使用时机
1. 针对右值引用的最后一次使用实施std::move,针对万能引用的最后一次使用实施std::forward。
2. 在按值返回的函数中,如果返回的是一个绑定到右值引用或万能引用的对象时,可以实施std::move或std::forward。因为如果原始对象是一个右值,它的值就应当被移动到返回值上,而如果是左值,就必须通过复制构造出副本作为返回值。
(三)返回值优化(RVO)
1.两个前提条件
(1)局部对象类型和函数返回值类型相同;
(2)返回的就是局部对象本身(含局部对象或作为return 语句中的临时对象等)
2. 注意事项
(1)在RVO的前提条件被满足时,要么避免复制,要么会自动地用std::move隐式实施于返回值。
(2)按值传递的函数形参,把它们作为函数返回值时,情况与返回值优化类似。编译器这里会选择第2种处理方案,即返回时将形参转为右值处理。
(3)如果局部变量有资格进行RVO优化,就不要把std::move或std::forward用在这些局部变量中。因为这可能会让返回值丧失优化的机会。
【编程实验】RVO优化和std::move、std::forward
#include <iostream> #include <memory> using namespace std; //1. 针对右值引用实施std::move,针对万能引用实施std::forward class Data{}; class Widget { std::string name; std::shared_ptr<Data> ptr; public: Widget() { cout <<"Widget()"<<endl; }; //复制构造函数 Widget(const Widget& w):name(w.name), ptr(w.ptr) { cout <<"Widget(const Widget& w)" << endl; } //针对右值引用使用std::move Widget(Widget&& rhs) noexcept: name(std::move(rhs.name)), ptr(std::move(rhs.ptr)) { cout << "Widget(Widget&& rhs)" << endl; } //针对万能引用使用std::forward。 //注意,这里使用万能引用来替代两个重载版本:void setName(const string&)和void setName(string&&) //好处就是当使用字符串字面量时,万能引用版本的效率更高。如w.setName("SantaClaus"),此时字符串会被 //推导为const char(&)[11]类型,然后直接转给setName函数(可以避免先通过字量面构造临时string对象)。 //并将该类型直接转给name的构造函数,节省了一个构造和释放临时对象的开销,效率更高。 template<typename T> void setName(T&& newName) { if (newName != name) { //第1次使用newName name = std::forward<T>(newName); //针对万能引用的最后一次使用实施forward } } }; //2. 按值返回函数 //2.1 按值返回的是一个绑定到右值引用的对象 class Complex { double x; double y; public: Complex(double x =0, double y=0):x(x),y(y){} Complex& operator+=(const Complex& rhs) { x += rhs.x; y += rhs.y; return *this; } }; Complex operator+(Complex&& lhs, const Complex& rhs) //重载全局operator+ { lhs += rhs; return std::move(lhs); //由于lhs绑定到一个右值引用,这里可以移动到返回值上。 } //2.2 按值返回一个绑定到万能引用的对象 template<typename T> auto test(T&& t) { return std::forward<T>(t); //由于t是一个万能引用对象。按值返回时实施std::forward //如果原对象一是个右值,则被移动到返回值上。如果原对象 //是个左值,则会被拷贝到返回值上。 } //3. RVO优化 //3.1 返回局部对象 Widget makeWidget() { Widget w; return w; //返回局部对象,满足RVO优化两个条件。为避免复制,会直接在返回值内存上创建w对象。 //但如果改成return std::move(w)时,由于返回值类型不同(Widget右值引用,另一个是Widget) //会剥夺RVO优化的机会,就会先创建w局部对象,再移动给返回值,无形中增加一个移动操作。 //对于这种满足RVO条件的,当某些情况下无法避免复制的(如多路返回),编译器仍会默认地对 //将w转为右值,即return std::move(w),而无须用户显式std::move!!! } //3.2 按值形参作为返回值 Widget makeWidget(Widget w) //注意,形参w是按值传参的。 { //... return w; //这里虽然不满足RVO条件(w是形参,不是函数内的局部对象),但仍然会被编译器优化。 //这里会默认地转换为右值,即return std::move(w) } int main() { cout <<"1. 针对右值引用实施std::move,针对万能引用实施std::forward" << endl; Widget w; w.setName("SantaClaus"); cout << "2. 按值返回时" << endl; auto t1 = test(w); auto t2 = test(std::move(w)); cout << "3. RVO优化" << endl; Widget w1 = makeWidget(); //按值返回局部对象(RVO) Widget w2 = makeWidget(w1); //按值返回按值形参对象 return 0; } /*输出结果 1. 针对右值引用实施std::move,针对万能引用实施std::forward Widget() 2. 按值返回时 Widget(const Widget& w) Widget(Widget&& rhs) 3. RVO优化 Widget() Widget(Widget&& rhs) Widget(const Widget& w) Widget(Widget&& rhs) */
四、完美转发失败的情形
(一)完美转发失败
1. 完美转发不仅转发对象,还转发其类型、左右值特征以及是否带有const或volation等修饰词。而完美转发的失败,主要源于模板类型推导失败或推导的结果是错误的类型。
2. 实例说明:假设转发的目标函数f,而转发函数为fwd(天然就应该是泛型)。函数如下:
template<typename… Ts> void fwd(Ts&&… params) { f(std::forward<Ts>(params)…); } f(expression); //如果本语句执行了某操作 fwd(expression); //而用同一实参调用fwd则会执行不同操作,则称完美转发失败。
(二)五种完美转发失败的情形
1. 使用大括号初始化列表时
(1)失败原因分析:由于转发函数是个模板函数,而在模板类型推导中,大括号初始不能自动被推导为std::initializer_list<T>。
(2)解决方案:先用auto声明一个局部变量,再将该局部变量传递给转发函数。
2. 0和NULL用作空指针时
(1)失败原因分析:0或NULL以空指针之名传递给模板时,类型推导的结果是整型,而不是所希望的指针类型。
(2)解决方案:传递nullptr,而非0或NULL。
3. 仅声明static const 整型成员变量,而无其定义时。
(1)失败原因分析:C++中常量一般是进入符号表的,只有对其取地址时才会实际分配内存。调用f函数时,其实参是直接从符号表中取值,此时不会发生问题。但当调用fwd时由于其形参是万能引用,而引用本质上是一个可解引用的指针。因此当传入fwd时会要求准备某块内存以供解引用出该变量出来。但因其未定义,也就没有实际的内存空间, 编译时可能失败(取决于编译器和链接器的实现)。
(2)解决方案:在类外定义该成员变量。注意这声变量在声明时一般会先给初始值。因此定义时无需也不能再重复指定初始值。
4. 使用重载函数名或模板函数名时
(1)失败原因分析:由于fwd是个模板函数,其形参没有任何关于类型的信息。当传入重载函数名或模板函数(代表许许多多的函数)时,就会导致fwd的形参不知绑定到哪个函数上。
(2)解决方案:在调用fwd调用时手动为形参指定类型信息。
5. 转发位域时
(1)失败原因分析:位域是由机器字的若干任意部分组成的(如32位int的第3至5个比特),但这样的实体是无法直接取地址的。而fwd的形参是个引用,本质上就是指针,所以也没有办法创建指向任意比特的指针。
(2)解决方案:制作位域值的副本,并以该副本来调用转发函数。
【编程实验】完美转发失败的情形及解决方案
#include <iostream> #include <vector> using namespace std; //1. 大括号初始化列表 void f(const std::vector<int>& v) { cout << "void f(const std::vector<int> & v)" << endl; } //2. 0或NULL用作空指针时 void f(int x) { cout << "void f(int x)" << endl; } //3. 仅声明static const的整型成员变量而无定义 class Widget { public: static const std::size_t MinVals = 28; //仅声明,无定义(因为静态变量需在类外定义!) }; //const std::size_t Widget::MinVals; //在类外定义,无须也不能重复指定初始值。 //4. 使用重载函数名或模板函数名 int f(int(*pf)(int)) { cout <<"int f(int(*pf)(int))" << endl; return 0; } int processVal(int value) { return 0; } int processVal(int value, int priority) { return 0; } //5.位域 struct IPv4Header { std::uint32_t version : 4, IHL : 4, DSCP : 6, ECN : 2, totalLength : 16; //... }; template<typename T> T workOnVal(T param) //函数模板,代表许许多多的函数。 { return param; } //用于测试的转发函数 template<typename ...Ts> void fwd(Ts&& ... param) //转发函数 { f(std::forward<Ts>(param)...); //目标函数 } int main() { cout <<"-------------------1. 大括号初始化列表---------------------" << endl; //1.1 用同一实参分别调用f和fwd函数 f({ 1, 2, 3 }); //{1, 2, 3}会被隐式转换为std::vector<int> //fwd({ 1, 2, 3 }); //编译失败。由于fwd是个函数模板,而模板推导时{}不能自动被推导为std:;initializer_list<T> //1.2 解决方案 auto il = { 1,2,3 }; fwd(il); cout << "-------------------2. 0或NULL用作空指针-------------------" << endl; //2.1 用同一实参分别调用f和fwd函数 f(NULL); //调用void f(int)函数, fwd(NULL); //NULL被推导为int,仍调用void f(int)函数 //2.2 解决方案:使用nullptr f(nullptr); //匹配int f(int(*pf)(int)) fwd(nullptr); cout << "-------3. 仅声明static const的整型成员变量而无定义--------" << endl; //3.1 用同一实参分别调用f和fwd函数 f(Widget::MinVals); //调用void f(int)函数。实参从符号表中取得,编译成功! fwd(Widget::MinVals); //fwd的形参是引用,而引用的本质是指针,但fwd使用到该实参时需要解引用 //这里会因没有为MinVals分配内存而出现编译失败(取决于编译器和链接器) //3.2 解决方案:在类外定义该变量 cout << "-------------4. 使用重载函数名或模板函数名---------------" << endl; //4.1 用同一实参分别调用f和fwd函数 f(processVal); //ok,由于f形参为int(*pf)(int),带有类型信息,会匹配int processVal(int value) //fwd(processVal); //error,fwd的形参不带任何类型信息,不知该匹配哪个processVals重载函数。 //fwd(workOnVal); //error,workOnVal是个函数模板,代表许许多多的函数。这里不知绑定到哪个函数 //4.2 解决方案:手动指定类型信息 using ProcessFuncType = int(*)(int); ProcessFuncType processValPtr = processVal; fwd(processValPtr); fwd(static_cast<ProcessFuncType>(workOnVal)); //调用int f(int(*pf)(int)) cout << "----------------------5. 转发位域时---------------------" << endl; //5.1 用同一实参分别调用f和fwd函数 IPv4Header ip = {}; f(ip.totalLength); //调用void f(int) //fwd(ip.totalLength); //error,fwd形参是引用,由于位域是比特位组成。无法创建比特位的引用! //解决方案:创建位域的副本,并传给fwd auto length = static_cast<std::uint16_t>(ip.totalLength); fwd(length); return 0; } /*输出结果 -------------------1. 大括号初始化列表--------------------- void f(const std::vector<int> & v) void f(const std::vector<int> & v) -------------------2. 0或NULL用作空指针------------------- void f(int x) void f(int x) int f(int(*pf)(int)) int f(int(*pf)(int)) -------3. 仅声明static const的整型成员变量而无定义-------- void f(int x) void f(int x) -------------4. 使用重载函数名或模板函数名--------------- int f(int(*pf)(int)) int f(int(*pf)(int)) int f(int(*pf)(int)) ----------------------5. 转发位域时--------------------- void f(int x) void f(int x) */