概述
std::forward是C++11中引入的一个函数模板,用于实现完美转发(Perfect Forwarding)。它的作用是根据传入的参数,决定将参数以左值引用还是右值引用的方式进行转发。
然而,完美转发是为了解决传递参数时的临时对象(右值)被强制转换为左值的问题。在C++03中,可以使用泛型引用来实现完美转发,但是需要写很多重载函数,非常繁琐。而在C++11中,引入了std::forward,可以更简洁地实现完美转发。
因此,概括来说,std::forward实现完美转发主要用于以下场景:提高模板函数参数传递过程的转发效率。
下面我们将逐步引入完美转发的必要性和用法。完美转发主要通过“引用折叠”和“std::forward”函数实现,我们先来了解他们。
引用折叠
C++引用折叠是一种特性,允许在模板元编程中使用引用类型的参数来创建新的引用类型。
由于存在T&&
这种万能引用类型,当它作为参数
时,有可能被一个左值引用或右值引用的参数初始化,这是经过类型推导的T&&类型,推导后得到的参数类型会发生类型变化,这种变化就称为引用折叠。
引用折叠的具体规则如下:
- 若一个右值引用(即带有
&&
)参数被一个左值或左值引用初始化,那么引用将折叠为左值引用。(即:T&& & –> T&) - 若一个右值引用参数被一个右值初始化,那么引用将折叠为右值引用。(即:T&& && 变成 T&&)。
- 若一个左值引用参数被一个左值或右值初始化,那么引用不能折叠,仍为左值引用(即:T& & –>T&,T& && –>T&)。
总结一下: 所有右值引用折叠到右值引用上仍然是一个右值引用。(A&& && 变成 A&&) 。 所有的其他引用类型之间的折叠都将变成左值引用。 (A& & 变成 A&; A& && 变成 A&; A&& & 变成 A&)。
简单来说:右值经过T&&参数传递,类型保持不变还是右值(引用);而左值经过T&&变为普通的左值引用。
为了更好地理解引用折叠,以下为几个示例:
template<typename T> void func(T&& arg); int main() { int a = 5; func(a); // arg为int&,引用折叠为左值引用 func(10); // arg为int&&,引用折叠为右值引用 int& ref = a; func(ref); // arg为int&,引用不能折叠 }
- 当func(a)时,参数类型折叠后实际为int&,因为a是一个左值,引用类型折叠为左值引用。
- 当func(10)时,参数类型折叠后实际为int&&,因为10是一个右值,引用类型折叠为右值引用。
- 当func(ref)时,参数类型折叠后实际为int&,由于左值引用类型不能折叠,参数类型保持为左值引用。
引用折叠是C++中模板编程中非常有用的特性,可以根据传递实参的左值还是右值来确定引用类型,进而使得编写通用的模板函数或类更简单。
std::forward函数
实现完美转发的关键是使用std::forward函数。std::forward是一个条件转发函数模板,根据参数的左值或右值属性进行转发。它的定义参考如下:
这个模板函数接受一个参数并返回一个右值引用,同时利用引用折叠保留参数的左值或右值属性。调用std::forward时,根据参数的左值或右值属性,编译器会选择适当的模板实例进行转发。如果参数是一个左值引用,std::forward将返回一个左值引用。如果参数是一个右值引用,std::forward将返回一个右值引用。
例如:
1、如果T为std::string&,那么std::forward(t) 返回值为std::string&& &,折叠为std::string&,左值引用特性不变。
2、如果T为std::string&&,那么std::forward(t) 返回值为std::string&& &&,折叠为std::string&&,右值引用特性不变。
掌握了以上知识之后,我们可能还是不清楚std::forward到底有什么用,那么请看下一节。
利用std::forward实现完美转发
C++完美转发是指一种能够传递函数参数或对象的同样类型(例如左值或右值属性)和cv限定符(const或volatile)的方式,同时保留原参数的准确数值类别和cv限定符的转发机制。完美转发通过使用引用折叠机制和std::forward函数来实现。
在C++11之前,当我们将一个参数转发给另一个函数时,会丢失参数的左值或右值的信息。例如,如果我们有一个函数f,它接受一个左值引用,然后我们通过f来调用一个函数g并传递一个右值,那么在g函数内部,参数将被视为左值,从而可能引入额外的参数转移开销。
C++11引入了右值引用、移动构造函数、引用折叠、std::forward等概念,使我们能够更准确地传递参数的左值或右值属性。因此,完美转发的目标是在转发参数时保持原始参数的左值或右值属性,从而提高函数参数传递的效率。
完美转发应用实例
首先定义一个对象CData,具体说明看注释:
#include <stdio.h> #include <unistd.h> #include <iostream> class CData { public: CData() = delete; CData(const char* ch) : data(ch) // 构造函数,涉及资源的复制 { std::cout << "CData(const char* ch)" << std::endl; } CData(const std::string& str) : data(str) // 拷贝构造函数,涉及资源的复制 { std::cout << "CData(const std::string& str)" << std::endl; } CData(std::string&& str) : data(str) // 移动构造函数,不涉及资源的复制!!! { std::cout << "CData(std::string&& str)" << std::endl; } ~CData() // 析构函数 { std::cout << "~CData()" << std::endl; } private: std::string data; // 表示类内部管理的资源 };
假如我们封装了一个操作,主要是用来创建对象使用(类似设计模式中的工厂模式),这个操作要求如下:
1. 可以接受不同类型的参数,然后构造一个对象的指针。
2. 性能尽可能高。(这里需要高效率,故对于右值的调用应该使用CData(std::string&& str)移动函数操作)
1)不使用std::forward实现
假设我们不使用std::forward,那么要提高函数参数转发效率,我们使用右值引用(万能引用)作为模板函数参数:
template<typename T> CData* Creator(T&& t) { // 利用&&万能引用,引用折叠: T&& && -> T&&; T&& & -> T& return new CData(t); } int main(void) { std::string str1 = "hello"; std::string str2 = " world"; CData* p1 = Creator(str1); // 参数折叠为左值引用,调用CData构造函数 CData* p2 = Creator(str1 + str2);// 参数折叠为右值引用,但在Creator函数中t仍为左值,调用CData构造函数!!! delete p2; delete p1; return 0; }
g++编译上述程序,可得如下结果,印证了注释中的说明:
可以看出,在不使用std::forward的情况下,即使传入了右值引用,也无法在Creator函数中触发CData的移动构造函数,从而造成了额外的资源复制损耗。
2)使用std::forward实现
使用std::forward即可完美解决上述问题:
template<typename T> CData* Creator(T&& t) { return new CData(std::forward<T>(t)); } int main(void) { std::string str1 = "hello"; std::string str2 = " world"; CData* p1 = Creator(str1); // 参数折叠为左值引用,调用CData构造函数 CData* p2 = Creator(str1 + str2); // 参数折叠为右值引用,通过std::forward转发给CData,调用移动构造函数 delete p2; delete p1; return 0; }
g++编译上述程序,可得如下结果,印证了注释中的说明:
可以看出,使用了std::forward之后,可以将传入的函数参数按照其原类型进一步传入参数中,从而使右值引用的参数类型可以触发类的移动构造函数,从而避免不必要的资源复制操作,提高参数转移效率。
结论:
所谓的完美转发,是指std::forward会将输入的参数原封不动地传递到下一个函数中,这个“原封不动”指的是,如果输入的参数是左值,那么传递给下一个函数的参数的也是左值;如果输入的参数是右值,那么传递给下一个函数的参数的也是右值。
完美转发主要使用两步来完成任务: 1. 在模板中使用&&(万能引用)接收参数。 2. 使用std::forward()转发给被调函数.
这个对于上面一个例子带来的好处就是函数转发仍旧为右值引用,可以使用移动构造函数提高参数转移的效率(关于移动构造函数可以参考上一篇文章《C++编程系列笔记(2)——std::move和移动语义详解》中的内容)。
标签:std,右值,16,左值,引用,&&,forward From: https://www.cnblogs.com/zwj-199306231519/p/17987070