自学内容网 自学内容网

【模板进阶】完美转发

一、完美转发的概念

首先,在开始介绍完美转发之前,我们先介绍几个相关概念:

( 1 ) (1) (1) 直接调用: 如从 m a i n main main函数中调用 f u n c M i d d l e ( ) funcMiddle() funcMiddle()函数,这其实就叫作直接调用。

( 2 ) (2) (2) 转发:从 m a i n main main主函数中调用 f u n c M i d d l e ( ) funcMiddle() funcMiddle()函数,然后通过 f u n c M i d d l e ( ) funcMiddle() funcMiddle()调用 f u n c L a s t ( ) funcLast() funcLast()函数,这就叫转发。

( 3 ) (3) (3) 完美转发:在转发(中转这些参数)的过程中,这些参数的某些类型信息回丢失(如参数的 c o s n t cosnt cosnt属性,引用( & \& &)等),显然这种转发就是不完美的。但是如果这些参数的类型信息可以原封不动地从 f u n c M i d d l e funcMiddle funcMiddle转发到 f u n c L a s t funcLast funcLast,那么这样的转发就成为完美转发。


二、普通转发和万能引用转发

2.1普通转发

我们先来看一个普通的转发函数模板(按值传递),函数里面打印了传入的参数,用于判断是否是完美转发:

//普通转发模板
template<typename F, typename T1, typename T2>
void funcMiddle_Temp(F f, T1 t1, T2 t2) {
std::cout << "--------------------------begin--------------------------\n";
std::cout << "type t1  = " << type_id_with_cvr<decltype(t1)>().pretty_name() << "\n";
std::cout << "type t2  = " << type_id_with_cvr<decltype(t2)>().pretty_name() << "\n";
std::cout << "--------------------------end--------------------------\n";

f(t1, t2);
}

void func1(int i, int& j) {
std::cout << "调用了func1\n";
j++;
}

void Test1() {
int j = 10;
funcMiddle_Temp(func1, 1, j); //此时转发出现了问题,丢失了引用&

std::cout << j << "\n";
}

首先我们将函数指针和参数传入转发模板,然后通过转发模板来调用 f u n c 1 func1 func1函数。
运行结果如下:
在这里插入图片描述

无论从类型推断来看,还是从 j j j的数据来看,传入的参数确确实实丢失了 & \& &限制,但这可能是由于转发模板的形参是值传递的原因。


2.1 万能引用转发

现在,我们为了避免值传递带来的影响,使用万能引用来传递参数:

//使用万能引用来转发

void func1(int i, int& j) {
std::cout << "调用了func1\n";
j++;
}

template<typename F, typename T1, typename T2>
void funcMiddle_Temp2(F f, T1&& t1, T2&& t2) {
std::cout << "--------------------------begin--------------------------\n";
std::cout << "type t1  = " << type_id_with_cvr<decltype(t1)>().pretty_name() << "\n";
std::cout << "type t2  = " << type_id_with_cvr<decltype(t2)>().pretty_name() << "\n";
std::cout << "--------------------------end--------------------------\n";
//转发后的t1,t2都是左值

f(t1, t2);
}


void Test2() {
int j = 10;
funcMiddle_Temp2(func1, 1, j); //成功转发

std::cout << j << "\n";
}

预判一下,这样推导出的 T 1 T1 T1 T 2 T2 T2由于是万能引用,所以不会丢失信息,运行一下:
在这里插入图片描述

确实是,分别被推导为了右值引用和左值引用,这样传递参数减少了拷贝的开销,并且能够保证信息的完整性。


然而,这就是完美转发了吗? 我们换一个最终函数 f u n c 2 func2 func2,形参中需要传递右值,如下:

void func2(int&& i, int& j) {
std::cout << i << "\n";
std::cout << j << "\n";
i++, j++;
}

T e s t 2 Test2 Test2中加入这一句:

funcMiddle_Temp2(func2, 1, j); //编译失败,无法从T&转为T&&

编译器报错:无法从T&转为T&&。我们知道,用于接受实参的形参类型是万能引用,而无论是左值引用还是右值引用,都是左值,当一个左值尝试传递到一个右值上是,必然会发生错误!

那么我们该如何修改呢?使用 s t d : : m o v e std::move std::move固然可以将左值转为右值。然而,原本形参中需要接受的左值也变成了右值,这样固然不行。

那么,此时就需要引入 s t d : : f o r w a r d < > std::forward<> std::forward<>了,这是一个专门为完美转发引入的模板。


三、使用 s t d : : f o r w a r d std::forward std::forward完美转发

3.1 s t d : : f o r w a d std::forwad std::forwad的用法

s t d : : f o r w a r d std::forward std::forward C + + 11 C++11 C++11标准库提供的专门为了转发而存在的函数模板,这个函数模板要么返回一个左值,要么返回一个右值。

具体而言, s t d : : f o r w a d std::forwad std::forwad是返回参数传入之前的状态。

( 1 ) (1) (1)原来是右值,转发后变为了左值,那么使用 s t d : : f o r w a r d std::forward std::forward后就会变回右值。
( 2 ) (2) (2)如果一开始是左值,由于传入后还是左值,那么使用了 s t d : : f o r w a r d std::forward std::forward后还是左值,不会发生变化。

参考下方代码,我们来实验一下:

/std::forward的使用

void printInfo(int &t) {
std::cout << "printInfo的左值引用版本\n";
}

void printInfo(int&& t) {
std::cout << "printInfo的右值引用版本\n";
}

void printInfo(const int& t) {
std::cout << "printInfo的const左值引用版本\n";
}

void printInfo(const int&& t) {
std::cout << "printInfo的const右值引用版本\n";
}


template<typename T>
void testF(T&& t) {
std::cout << "--------------------------begin--------------------------\n";
std::cout << "type t  = " << type_id_with_cvr<decltype(t)>().pretty_name() << "\n";

printInfo(std::forward<T>(t));
std::cout << "--------------------------end--------------------------\n";


}

void Test3() {
int i = 1;
const int j = 2;
int k = 3;
const int& n = i;
int&& m = 6;
int& x = i;
testF(n);
testF(m);
testF(x);

testF(i);
testF(j);
testF(std::move(k));
testF(std::move(j));
testF(4);
testF(int(5));
}

注意这里转发函数模板中的这一句的写法,这是完美转发的核心

printInfo(std::forward<T>(t));

测试结果如下:
在这里插入图片描述
可以发现,在传递参数和转发参数的过程中,左值右值的信息都完美的转发到了相应的函数中了。


3.1 使用 s t d : : f o r w a d std::forwad std::forwad实现完美转发

我们了解了 s t d : : f o r w a r d std::forward std::forward的特性和用法之后,就可以通过它实现完美转发了,参考下方代码:

//使用std::forward实现完美转发
template<typename F, typename T1, typename T2>
void funcMiddle_Temp3(F f, T1&& t1, T2&& t2) {

std::cout << "--------------------------begin--------------------------\n";
std::cout << "type t1  = " << type_id_with_cvr<decltype(t1)>().pretty_name() << "\n";
std::cout << "type t2  = " << type_id_with_cvr<decltype(t2)>().pretty_name() << "\n";
std::cout << "--------------------------end--------------------------\n";

//把参数转换为转发前的类型,这里把t1变为原来的右值
f(std::forward<T1>(t1), std::forward<T2>(t2));

//不改变原先的类型
std::cout << "--------------------------begin--------------------------\n";
std::cout << "type t1  = " << type_id_with_cvr<decltype(t1)>().pretty_name() << "\n";
std::cout << "type t2  = " << type_id_with_cvr<decltype(t2)>().pretty_name() << "\n";
std::cout << "--------------------------end--------------------------\n";


}

void Test4() {
int i = 10;
int j = 10;
funcMiddle_Temp3(func2, std::move(i), j);

std::cout << i << " " << j << "\n"; //成功修改
}

可以发现,成功编译了,我们运行一下:
在这里插入图片描述可以发现,类型正确,并且成功在函数内被修改了。


三、完美转发类型

3.1对返回值进行完美转发

有时,我们需要对函数的返回值进行修改,然后再进行转发,那么此时对于函数返回值的接受就需要使用万能引用,转发就要使用完美转发。
这里我们使用 a u t o & & auto\&\& auto&&作为万能引用接受返回值,参考下方代码:

int myfunc() {
return 1;
}

void myfunc2(int&& x) {
std::cout << x << "\n";

}

//转发函数
void funcMiddle_Temp() {
auto&& res = myfunc(); //万能引用

//对返回值进行操作
res++;

myfunc2(std::forward<decltype(res)>(res)); //将左值转为右值转发

}


void Test5() {
funcMiddle_Temp();
}

没什么问题,参数成功被修改了:

在这里插入图片描述


3.2 在构造函数模板中使用完美转发

首先,我们先定义一个 H u m a n Human Human类,然后写入其相应的构造函数,这里我们先使用完美转发,观察一下结果:

class Human {
private:
std::string m_name;
public:
Human(const std::string& tmpname) :m_name(tmpname) { //可以接受左值和右值
std::cout << "Human(const std::string & tmpname)执行了\n";
}
//只接受右值
Human(std::string&& tmpname) :m_name(std::move(tmpname)) { //可以接受右值
std::cout << "Human(std::string&& tmpname)执行了\n";
}
};

我们分别实现了用于接受左值的构造函数和用于接受右值的构造函数,其中,后者使用了 s t d : : m o v e std::move std::move将左值的形参转为右值,用于调用 s t d : : s t r i n g std::string std::string的移动构造。

这种写法是没问题,不过我们可以通过 s t d : : f o r w a r d std::forward std::forward将这两种写法合二为一。


参考下方代码:

//完美转发构造函数模板
template<typename T>
Human(T&& tmpname) :m_name(std::forward<T>(tmpname)) {
std::cout << "Human(T&& tmpname)执行了\n";
}

这里的 T & & T\&\& T&&是万能引用,然后通过 s t d : : f o r w a r d std::forward std::forward将参数完美转发到 s t d : : s t r i n g std::string std::string的构造函数里面,这样的写法比较简洁。


注意,此时我们添加一个拷贝构造函数:

//拷贝构造函数
Human(const Human& th) :m_name(th.m_name) {
std::cout << "Human(Human const&th)执行了\n";
}

然后调用这一段代码:


std::string sname = "ZhangSan";
Human myhuman1(sname);

Human myhuman2(std::string("LiSi"));

Human myhuman3(myhuman1);//编译失败

其中, H u m a n   m y h u m a n 3 ( m y h u m a n 1 ) ; Human \ myhuman3(myhuman1); Human myhuman3(myhuman1);将编译失败,原因是编译器选择了构造函数模板而不是靠北构造函数!

这个问题,我们可以通过 s t d : : f a l s e _ t y p e std::false\_type std::false_type s t d : : i s _ c o n v e r t i b l e std::is\_convertible std::is_convertible来解决,这里暂时不提。


3.3 在可变参模板中使用完美转发

3.3.1 不转发返回值的完美转发

在可变参模板中使用完美转发也是可以的,只不过要注意一下语法上的细节:


//在可变参模板中使用完美转发

template<typename F,typename...T>
void my_func_Middle_Temp(F f,T&&... t) {
f(std::forward<T>(t)...); //注意写法
}

void funcLast(int v1,int &v2) {
++v2;
std::cout << v1 + v2 << "\n";
}

void Test7() {
int j = 70;
my_func_Middle_Temp(funcLast, 20, j);

std::cout <<"j = "<< j << "\n";
}

注意这里 f ( s t d : : f o r w a r d < T > ( t ) . . . ) f(std::forward<T>(t)...) f(std::forward<T>(t)...)的写法。


3.3.2 转发返回值的完美转发

如果可变参数模板存在返回值的话,转发会复杂一点,一般情况下,我们可以写成下面的代码:


//将目标函数的返回值转发给调用者函数
int funcLast2(int v1,int &v2) {
++v2;
std::cout << v1 + v2 << "\n";
return v1 + v2;
}

//使用auto+decltype来推导返回值,也可以直接使用auto
template<typename F, typename...T>
auto my_func_Middle_Temp2(F f, T&&... t) -> decltype(f(std::forward<T>(t)...)) {

return f(std::forward<T>(t)...); //注意写法
}

void Test8() {
int j = 70;
int  k = my_func_Middle_Temp2(funcLast2, 20, j);

std::cout << "j = " << j << "\n";
std::cout << "k = " << k << "\n";


}

但是,这里存在一个问题,如果 f f f是一个函数模板,并且返回值是一个引用类型,那么使用 d e c l t y e decltye decltye就会丢失掉引用这个信息。


为了解决这个问题,我们可以使用 d e c l t y p e ( a u t o ) decltype(auto) decltype(auto)来推导返回值,这样不会丢失信息:

//更好的写法,避免f是一个函数模板,并且返回值带有引用/const限定等等
template<typename F, typename...T>
decltype(auto) my_func_Middle_Temp2(F f, T&&... t){

return f(std::forward<T>(t)...); //注意写法
}

四、完美转发失败的情况

这里介绍一种常见的失败,参考下方代码:

//完美转发失败的情况

void funcLast3(char* p) {
if (p != NULL) {
strncpy(p, "abc", 3);
std::cout << p << "\n";
}
else {
std::cout << "指针为空\n";
}
}

template<typename F,typename T>
void my_funcTemp2(F f, T&& t) {
f(std::forward<T>(t));
}

调用下方代码:

void Test9() {
char* p = new char[100];
memset(p, 0, 100);
my_funcTemp2(funcLast3, p);

delete []p;

my_funcTemp2(funcLast3, NULL); //编译失败,因为NULL是0,无法从int转为char*

m y _ f u n c T e m p 2 ( f u n c L a s t 3 , N U L L ) ; my\_funcTemp2(funcLast3, NULL); my_funcTemp2(funcLast3,NULL); 编译失败了,无法将 i n t int int转为 c h a r ∗ char* char。原因很简单,因为 N U L L NULL NULL是一个宏定义,本质上就是个 i n t int int类型 0 0 0


因此,在 C + + 11 C++11 C++11引入了 n u l l p t r nullptr nullptr来表示空指针,它可以隐式地转换为任意类型的指针,所以,需要使用 N U L L NULL NULL的地方都尽量替换为 n u l l p t r nullptr nullptr吧。

下面是正确的代码:

void funcLast3(char* p) {
if (p != nullptr) {
strncpy(p, "abc", 3);
std::cout << p << "\n";
}
else {
std::cout << "指针为空\n";
}
}

template<typename F,typename T>
void my_funcTemp2(F f, T&& t) {
f(std::forward<T>(t));
}

void Test9() {
char* p = new char[100];
memset(p, 0, 100);
my_funcTemp2(funcLast3, p);

delete []p;

my_funcTemp2(funcLast3, nullptr); //编译成功,nullptr被看做指针

}

原文地址:https://blog.csdn.net/Antonio915/article/details/142381135

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!