一. 理解引用折叠
(一)引用折叠
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); //可能会发生引用折叠! }
(二)分析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) */