文章目录
完美转发的实现要依赖于模版类型推导和引用折叠和万能引用。
1. 引用折叠
- 在C++中,“引用的引用"是非法的。像
auto& & rx = x;
(两个引用之间有空格),这种直接定义引用的引用是不合法的,但是在编译器通过类型别名或模版类型推导等语境中,可能间接定义出"引用 的引用”,这是引用会形成"折叠" - 引用折叠会发生的场景:模版的实例化,auto类型推导,创建和运用typedef 和别名声明以及decltype语境
引用折叠规则:
所有的引用折叠最终都代表一个引用,要么是左值引用,要么是右值引用。
规则就是:
如果任一引用为左值引用,则结果为左值引用。否则(即两个都是右值引用),结果为右值引用。
-----根本原因是:在一处的声明为左值,就说明该对象为持久对象,编译器就必须此对象可靠(左值)
《Effective Modern C++》
x&& && 折叠为 x&&
x& &,x&& &, x& && 都折叠为 x&
编程实验:
class widget
{};
template<typename T>
void func(T&& param)
{}
widget widgetFactory()
{
return widget();
}
template<typename T>
class Foo
{
public:
typedef T&& RvalueRefToT;
};
int main()
{
int x = 0;
int& rx = x;
//折叠引用发生的场景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.---auto类型推导
auto&& w2 = w1; //w1为左值auto被推导为widget&,代入得:widget& &&, 引用折叠后:widget& w2;
auto&& w3 = widgetFactory(); //函数返回widget,为右值,auto被推导为widget,代入的: widget w3;
//引用折叠发生的语境3——tyedef和using
Foo<int&> f; //T被推导为int& ,代入得:typedef int& && RvalueRefToT,引用折叠后:typedef int& RvalueRefToT
//引用折叠发生的语境4——decltype
decltype(x) && var1 = 10; //由于x是int类型: 代入得 int&& var1;
decltype(rx) && var2 = x; //由于rx为int& 类型,代入得 int& var2;
return 0;
}
2. 万能引用
万能引用:既可以接受左值类型的参数又可以接受右值类型的参数。
T&&含义:
-
当T是一个具体的类型时,T&&表示右值引用,只能绑定到右值。
-
当涉及T类型推导时,T&&为万能引用。若用右值初始化万能引用,则T&&为右值引用。若用左值初始化万能引用,则T&&为左值引
条件:T&&是万能引用的两个条件:
(1)必须涉及类型推导;
(2)声明的形式也必须正好形如“T&&”。并且该形式被限定死了,任何对其修饰都将剥夺T&&成为万能引用的资格
template<typename T>
ReturnType Function(T&& parem)
{
// 函数功能实现
}
编程实验:
#include <iostream>
using namespace std;
void fun(int& x) { cout << "call lvalue ref" << endl; }
void fun(int&& x) { cout << "call rvalue ref" << endl; }
void fun(const int& x) { cout << "call const lvalue ref" << endl; }
void fun(const int&& x) { cout << "call const rvalue ref" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
std::cout << "T is a ref type?: " << std::is_reference<T>::value << std::endl;
std::cout << "T is a lvalue ref type?: " << std::is_lvalue_reference<T>::value << std::endl;
std::cout << "T is a rvalue ref type?: " << std::is_rvalue_reference<T>::value << std::endl;
fun(forward<T>(t));
}
int main()
{
PerfectForward(10); // call rvalue ref
int a = 5;
PerfectForward(a); // call lvalue ref
PerfectForward(move(a)); // call rvalue ref
const int b = 8;
PerfectForward(b); // call const lvalue ref
PerfectForward(move(b)); // call const rvalue ref
system("pause");
return 0;
}
/*输出结果
T is a ref type?: 0
T is a lvalue ref type?: 0
T is a rvalue ref type?: 0 //万能引用绑定右值,T会被推导为 T 类型
call rvalue ref
T is a ref type?: 1
T is a lvalue ref type?: 1
T is a rvalue ref type?: 0 //万能引用绑定左值,T会被推导为T& 类型
call lvalue ref
T is a ref type?: 0
T is a lvalue ref type?: 0
T is a rvalue ref type?: 0
call rvalue ref
T is a ref type?: 1
T is a lvalue ref type?: 1
T is a rvalue ref type?: 0
call const lvalue ref
T is a ref type?: 0
T is a lvalue ref type?: 0
T is a rvalue ref type?: 0
call const rvalue ref
*/
利用引用折叠进行万能引用初始化类型推导
- 当万能引用(T&& param )绑定到左值时,由于万能引用也是引用,而左值只能绑定到左值引用,因此,T会被推导为T& 类型,从而 param 的类型为 T& && ,引用折叠后类型变为: T&;
- 当万能引用(T&& param )绑定到左值时,右值只能绑定到右值引用,因此,T会被推导为 T 类型,从而 param 的类型为 T&&(右值引用)。
总结:万能引用就是利用模板推导和引用折叠的相关规则,生成不同的实例化模板来接收传进来的参数。
3. 完美转发
指的是 函数模板可以将自己的参数 “完美” 地转发给内部调用的其它函数 ,不仅能准确地转发参数的值,还能 保证被转发参数的左、右值属性不变 。
//左值版本
template<class T>
constexpr T&& forward(std::remove_reference_t<T>& arg) noexcept
{
// forward an lvalue as either an lvalue or an rvalue
return (static_cast<T&&>(arg));//可能发生引用折叠
}
//右值版本
template<class T>
constexpr T&& forward(std::remove_reference_t<T>&& arg) noexcept
{
// forward an rvalue as an rvalue
return (static_cast<T&&>(arg));
}
为什么引入完美转发?
编程实验:
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&& parm)
{
//不完美转发
print(parm); //parm 是形参,是左值。调用void print(const int& t),永远调用左值版本
print(std::move(parm)); //parm是 右值。调用void print(int&& t),永远调用右值版本
//完美转发
print(std::forward<T>(parm));//只有这里才会根据传入parm的实参类型的左右值进行转发
}
int main()
{
cout << "测试右值:" << endl;
testForward(1);
cout << "测试左值:" << endl;
int x = 1;
testForward(x);
return 0;
}
/*输出结果:
测试右值
void print(const int& t)
void print(int&& t)
void print(int&& t) //完美转发,这里转入的值为1,为右值,调用右值版本的print
测试左值
void print(const int& t)
void print(int&& t)
void print(const int& t)//完美转发,这里转入的值为x,为左值,调用左值版本的print
*/
分析std:forward 实现条件转发的原理:以widget类对象为例
:::info
总结:
- 当传递给func函数的实参类型是左值widget时,T被推导为widget&类型,然后forward会实例化为std:forward<widget&>,并返回widget&(左值引用)。
- 当传递给func函数的实参类型是右值widget时,T被推导为widget类型,然后forward会实例化为std:forward,并返回widget&&(右值引用,注意:匿名的右值引用是右值)。
- std:forward会根据传递给func函数的实参的左/右值类型进行转发,当传递给func函数左值实参时,forward返回左值引用,并将改值转发给process函数,而当传递给func函数右值实参时,forward返回右值引用,并将右值转发给process函数。
:::
3.1对比:std::move and std::forward
比较
- move和forward都是仅仅执行强制类型转换的函数,std:move 无条件的将实参强制转换为右值。而std:forward 则仅是在某个特定的条件满足时(转入func函数的实参时右值)才进行强制类型转换。
- std:move 并不进行任何移动,std:forward 也不进行任何转发。这两者都在运行期间无所作为,他们都不会再生成任何可执行代码。连一个字节都不会生成。
3.2使用时机
- 针对右值引用的最后一次使用实施std:move,针对万能引用的最后一次使用实施std:forward。
- 在按值返回的函数中,如果返回的是一个绑定在右值引用或万能引用的对象时,可以实施std:move 或 std:forward。因为如果原始对象是右值,它的值就应被移动到返回值上,而如果是左值,就必须通过复制构造出副本作为返回值。
3.3 返回值优化(RVO)
两个前提条件
- 局部对象类型和函数返回值类型相同。
- 返回的就是局部对象本身。(包含局部对象和作为return语句中的临时对象等)。
注意事项
- 在RVO的前提条件满足时,要么避免复制,要么会自动调用std::move隐式实施于返回值。
- 按值传递的函数形,把他们作为函数的返回值时,情况与返回值优化类似,编译器会自动选择第二种处理方案,即返回时将形参转为右值处理。
- 如果局部变量有资格进行RVO优化,就不要将std:move 或 std:forward用在这些变量上,因为这可能会让返回值丧失优化的机会。
//针对右值引用实施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)
:_name(std::move(rhs._name))
, _ptr(std::move(rhs._ptr))
{
cout << "widget(widget&& w)" << endl;
}
//针对万能引用使用std:forward.
//注意这里使用万能引用来代替两个重载版本:void setName(const string& )和 void setName(string&&)
//好处是当使用字符串字面量时,万能引用的版本的效率更高,如:w.setName("santa"),此时字符串会被推导为
//const char (&)类型,然后直接转给setName函数,(可以避免先通过字面量来构造string对象),
//并将该类型直接转给name的构造函数,节省了一个构造和释放临时对象的开销,效率更高
template<class T>
void setNaem(T&& newname)
{
if (newname != _name)//第一次使用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, complex& rhs)
{
lhs += rhs;
return std::move(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对象。
//如果改为: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是形参,不是函数的局部对象),当时仍会被编译器优化,默认的转为右值
}
int main()
{
cout << "1. 针对右值引用实施std::move,针对万能引用实施std::forward" << endl;
widget w;
w.setNaem("SantaClaus");
cout << "2. 按值返回时" << endl;
auto t1 = test(w);
auto t2 = test(std::move(w));
cout << "3. RVO优化" << endl;
widget w1 = makewidget(); //按值返回局部对象
widget w2 = makewidget(w1); //按值返回形参对象
return 0;
}
//运行结果:
/*
1. 针对右值引用实施std::move,针对万能引用实施std::forward
widget()
2. 按值返回时
widget(const widget& w)
widget(widget&& w)
3. RVO优化
widget()
widget(widget&& w)
widget(const widget& w)
widget(widget&& w)
*/
4. 完美转发失败情况
完美转发失败
- 完美转发不仅转发对象,还转发其类型,左右值特征以及是否带有const 或 volation 等修饰词。而完美转发的失败,主要来源于模版类型推导失败或结果是错误的类型。
- 实例说明:假设转发的目标函数f,而转发函数fwd(天然就应该是泛型)
template<typename... Ts>
void fwd(Ts&&... param)
{
f(std::forward<Ts>(param)...);
}
f(experssion); //如果本语句执行了某操作
fwd(experssion);//而用同一实参调用fwd 则会执行不同操作,则完美转发失败
五种情况
- 使用{}初始化列表时
分析:由于转发函数是模版函数,而在模版类型推导中,大括号不能自动推导为std:initializer_list
解决方案:用auto 声明一个局部变量,在将局部变量传递给转发函数。
- 0和NULL作空指针时
分析:0或NULL以空指针之名传递给模版时,类型推导的结果是整型,而不是所希望的指针类型。
解决方案:传递nullptr,而非0或NULL。
- 仅声明static成员变量而无定义时
分析:C++中常量一般是进入符号表中的只有对其取地址是才会实际分配地址,调用f函数时,其实参是直接从符号表中取值,此时不会出现问题,但当调用fwd由于其形参是万能引用,而引用本质上是可解引用的指针。因此当传入fwd会要求准备某块内存以供解引用出该变量出来,但因其未定义,也就没有实际的内存空间,编译时可能失败。注意:如果是static const 声明的常量的值是整形,或者布尔型的,那么就可以直接在类中进行定义初始化,如果是浮点数和自定义类型,那么就需要再类外进行定义初始化,
解决方案:在类外定义该成员变量。注意着这变量在生声明时,一般会先给初始值,因此在定义时,无需也不能再重复指定初始值
- 使用重载函数名或模版函数名时
分析:由于fwd是模版类型参数,其形参没有任何类型的信息。当传入重载函数名或模版函数时(代表许多函数),就会导致fwd的形参不知绑定到哪一个函数上。
解决方案:在调用fwd函数时手动为形参指定类型信息。
- 转发位域时
分析:位域是由机器字的若干任意部分组成,(任意int的3 到 5个比特位),但这样的实体是无法取地址的,而fwd的形参是引用,本质就是指针,所以也没有办法创建指向任意比特的指针。
解决方法:制作位域的副本,并以该副本来调用转发函数。
//大括号初始化列表
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(intx)" << endl;
}
//3.仅声明static const的整型变量而无定义
class widget
{
public:
static const double MinValue ;//仅声明无定义(静态变量需要再类外定义)
};
const double widget::MinValue = 28.10;//在类外定义无需也不能重复定义初始值
//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}会被隐式转函为vector<int>
//fwd({ 1,2,3 });//编译失败,由于fwd是模版函数,而模版推导时{}不能被自动推导为std::initializar_list<int>
//解决方案:
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::MinValue); //调用用void f(int) 函数实参从符号表中取得,编译成功
fwd(widget::MinValue); //fwd的形参是引用,而引用的本质是指针,但fwd使用到该实参时需要解引用
//这里会因为没有为MinValue分配内存·而出现编译错误
//3.2解决方案:在类外定义该变量
cout << "-------------4. 使用重载函数名或模板函数名---------------" << endl;
//4.1 使用同一实参分别调用f和fwd函数
f(processVal); //由于形参是int(*pf)(int),带有类型信息会匹配int processVal(int value)
//fwd(processVal);//error,fwd的形参不带任何类型信息,不知到会匹配到哪个processVal函数
//4.2 解决方案
using processFuncType = int(*)(int);
processFuncType processValOPtr = processVal;
fwd(processValOPtr);
fwd(static_cast<processFuncType>(workonval));//调用int f((*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(intx)
void f(intx)
int f(int (*pf)(int))
int f(int (*pf)(int))
-------3. 仅声明static const的整型成员变量而无定义--------
void f(intx)
void f(intx)
-------------4. 使用重载函数名或模板函数名---------------
int f(int (*pf)(int))
int f(int (*pf)(int))
int f(int (*pf)(int))
----------------------5. 转发位域时---------------------
void f(intx)
void f(intx)
*/