C++11:深入理解完美转发(cpp 完美转发)
0.简介
本文将对C++11中完美转发特性进行详细分析,主要对什么是完美转发(概念),完美转发产生原因(动机)以及完美转发的原理(实现,主要包含万能引用,引用折叠以及std::forward函数),应用进行介绍。
1.什么是完美转发
完美转发(Perfect Forwarding)是 C++11 引入的一种技术,用于在函数模板中将参数原封不动地传递给另一个函数。这里的“原封不动”指的是:
1)保持参数的值类别(value category),即左值、右值。
2)保持参数的类型,包括 const、volatile 等修饰符。
完美转发的典型应用场景是泛型编程,例如实现一个通用的工厂函数或包装器,能够将参数传递给任意目标函数。
2.为什么需要完美转发
在C++中,函数参数的值的类别(左值或者右值)和类型信息在传递过程中可能会丢失,以下方函数为例:
#include<iostream>
void target(int& x) { std::cout << "lvalue\n"; }
void target(int&& x) { std::cout << "rvalue\n"; }
template<typename T>
void forwarder(T x) {
target(x); // 无论传入的是左值还是右值,x 都是左值
}
int main() {
int a = 10;
forwarder(a); // 期望调用 target(int&)
forwarder(10); // 期望调用 target(int&&)
return 0;
}
在上面代码中,forwarder函数无法区分传入的参数是左值还是右值,因为x在函数内部总是左值,想对这个进行区分,就需要引入完美转发。
3.实现原理
完美转发的核心是一下两个特性,一个是引用折叠(Reference Collapsing),其可以推导出万能引用,是完美转发的基础;另一个是std::forward函数。
3.1 引用折叠
3.1.1 右值引用
右值引用只能绑定到右值上,不能绑定左值。
#include <iostream>
using namespace std;
void myfunc(int&& val) // 右值引用
{
cout << val << endl;
return;
}
int main()
{
myfunc(120); // ok
int i = 180;
myfunc(i); // error
return 0;
}
3.1.2 万能引用
万能引用(Universal Reference)是 C++11 引入的一种引用类型,能够同时绑定到左值和右值。它的语法形式是 T&&,其中 T 是一个模板参数或 auto 推导的类型。万能引用的核心特点是:
1)如果传入的是左值,T&& 推导为左值引用(T&)。
2)如果传入的是右值,T&& 推导为右值引用(T&&)。
其折叠规则如下:
引用折叠前 | 引用折叠后 | |
左值引用-左值引用 | T& & | T& |
左值引用-右值引用 | T& && | T& |
右值引用-左值引用 | T&& & | T& |
右值引用-右值引用 | T&& && | T&& |
使用方式如下:
template<typename T>
void func(T&& arg); // T&& 是万能引用
auto&& var = value; // auto&& 是万能引用
#include <iostream>
#include <type_traits> // for std::is_lvalue_reference
template<typename T>
void func(T&& arg) {
if (std::is_lvalue_reference<T>::value) {
std::cout << "lvalue\n";
} else {
std::cout << "rvalue\n";
}
}
int main() {
int a = 10;
func(a); // 传入左值,T 推导为 int&
func(10); // 传入右值,T 推导为 int
return 0;
}
3.2 std::forward函数
std::forward函数本质上是一个条件转换函数,其定义如下:
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }
我们可以假设传进来的是左值或者右值引用,然后按照引用折叠方式来进行推导。
1)假设是int &传入,则得到如下内容:
constexpr int & && //折叠
forward(typename std::remove_reference<int &>::type& __t) noexcept
{ return static_cast<int & &&>(__t); } //折叠
//折叠之后就是
constexpr int & //折叠
forward(int& __t) noexcept
{ return static_cast<int &>(__t); } //折叠
2)假设传入的是int &&,则折叠后得到以下内容:
constexpr int && &&
forward(typename std::remove_reference<int &&>::type& __t) noexcept
{ return static_cast<int && &&>(__t); }
//折叠后如下
constexpr int &&
forward(int & __t) noexcept
{ return static_cast<int &&>(__t); }
4.完美转发的应用
4.1 通用模板函数
#include <iostream>
#include <utility> // for std::forward
// 目标函数
void target(int& x) { std::cout << "lvalue\n"; }
void target(int&& x) { std::cout << "rvalue\n"; }
// 完美转发函数模板
template<typename T>
void forwarder(T&& x) {
target(std::forward<T>(x)); // 完美转发
}
int main() {
int a = 10;
forwarder(a); // 调用 target(int&)
forwarder(10); // 调用 target(int&&)
return 0;
}
1)模板参数推导:
当传入左值 a 时,T 被推导为 int&,T&& 折叠为 int&。
当传入右值 10 时,T 被推导为 int,T&& 折叠为 int&&。
2)std::forward 的作用:
如果 x 是左值引用,std::forward<T>(x) 返回左值引用。
如果 x 是右值引用,std::forward<T>(x) 返回右值引用。
3)目标函数调用:
forwarder(a) 调用 target(int&)。
forwarder(10) 调用 target(int&&)。
4.2 应用场景
完美转发常见的应用场景有以下两种:
1)用于工厂函数:
template<typename T, typename... Args>
T create(Args&&... args) {
return T(std::forward<Args>(args)...);
}
struct Widget {
Widget(int a, double b) {
std::cout << "Widget constructed with " << a << " and " << b << "\n";
}
};
int main() {
auto w = create<Widget>(10, 3.14); // 完美转发参数
return 0;
}
2)用于包装器:
template<typename Func, typename... Args>
auto wrapper(Func&& func, Args&&... args) {
return std::forward<Func>(func)(std::forward<Args>(args)...);
}
void print(int a, double b) {
std::cout << a << ", " << b << "\n";
}
int main() {
wrapper(print, 10, 3.14); // 完美转发参数
return 0;
}
4.3 注意事项
1)避免过度使用:完美转发会增加代码的复杂性,只有在需要保持参数的值类别时才使用。
2)std::forward 的正确使用:std::forward 只能用于模板参数推导出的引用类型,不能用于普通变量。
3)性能开销:完美转发本身几乎没有性能开销,但可能会引入额外的模板实例化。