参数转发(Parameter Forwarding)是指在 C++ 中将函数参数原封不动地传递给另一个函数的技术。完美转发 (Perfect Forwarding) 则是为了在参数传递过程中避免不必要的拷贝或移动,从而保持参数的原始类型和属性。
- 保持参数类型和属性:在函数模板中,有时我们希望传递的参数能够保留其左值(lvalue)或右值(rvalue)的属性。这是因为左值引用和右值引用的行为和性能不同,特别是在涉及资源管理时(如内存、文件句柄等),直接影响程序的效率和正确性。
- 避免不必要的拷贝和移动:如果不能正确区分参数的左值和右值属性,在函数内部处理这些参数时可能会导致不必要的拷贝或移动操作,这会带来额外的开销和潜在的性能问题。

1. 问题场景
在 C++ 中,有很多 make 开头的工厂函数。我们也编写一个工厂函数,从中去发现一些问题。
#include <iostream>
using namespace std;
struct Person
{
Person(string&)
{
cout << "左值" << endl;
}
Person(string&&)
{
cout << "右值" << endl;
}
};
// 问题:
// 1. param 参数会导致对象发生拷贝、移动操作
// 2. param 传递到构造函数,无法区分值的类别(左值、右值)
Person make_person(string param)
{
// 如果传递的参数 param 是左值
return Person(param);
}
void test()
{
// 1. 传递左值
string name = "Obama";
make_person(name);
// 2. 传递右值
make_person(move(name));
}
int main()
{
test();
return 0;
}
程序执行结果:
左值
左值
2. 引用折叠
#include <iostream> #include <type_traits> using namespace std; struct Person { Person(string&) { cout << "左值" << endl; } Person(string&&) { cout << "右值" << endl; } }; // 思考过程: // 为了避免拷贝,需要使用引用参数 // 如果写左值引用,则无法匹配右值对象 // 如果写右值引用,则无法匹配左值对象 // 如果写常量引用,则无法匹配 Person 构造函数 // 所以,通过模板推导类型 // 写 T&,就无法匹配右值 // 写 const T& 会给传递进来的参数增加 const 性,也不合理 // 只能写 T&& // 写 T&& 可以匹配右值,为什么可以匹配左值? // 这就需要了解 C++11 中的引用折叠 // 函数要能够实现无拷贝,就需要实现对左值和右值的引用 template<class T> Person make_person(T&& param) { cout << "是否左值:" << is_lvalue_reference<T&&>::value << endl; cout << "是否右值:" << is_rvalue_reference<T&&>::value << endl; // 如果传递的参数 param 是左值 return Person(param); } void test() { // 1. 传递左值 string name = "Obama"; make_person(name); cout << "---------" << endl; // 2. 传递右值 make_person(move(name)); } int main() { test(); return 0; }
引用折叠是 C++11 引入的一个特性,它指的是当多种引用类型(左值引用或右值引用)组合在一起时,通过一组规则确定最终的引用类型。
- 如果任何一个引用是左值引用,则结果为左值引用。
- 否则(即所有引用都是右值引用),则结果为右值引用。
template<class T> Person make_person(T&& param) { // 如果传递的参数 param 是左值 return Person(param); } void test() { // 1. 传递左值 string name = "Obama"; make_person(name); // 2. 传递右值 make_person(move(name)); }
make_person(模板函数参数) | T 类型(传递参数类型) | 最终类型 |
T& | T& | T& |
T&& | T& | T& |
T&& | T&& | T&& |
- 如果 make_person 的参数设置为 T&
- 如果传递左值/右值,则 make_person 的 T& 最终将被确定为左值;
- 如果 make_person 的参数设置为 T&&
- 如果传递左值,则 make_person 的 T&& 最终将被确定为左值;
- 如果传递右值,则 make_person 的 T&& 最终将被确定为右值;
3. 完美转发
在前面的实现中,我们可以做到 make_person 在不拷贝的情况下,接收到外部传递的左值和右值参数。但是,再次将值转发到 Person 构造函数时,发现值的类型、值的性质(左值、右值)丢失,使得无法正确调用 Person 的构造函数。
#include <iostream>
#include <type_traits>
using namespace std;
struct Person
{
Person(string&)
{
cout << "左值" << endl;
}
Person(string&&)
{
cout << "右值" << endl;
}
};
// 问题:
// 1. param 参数会导致对象发生拷贝、移动操作
// 2. param 传递到构造函数,无法区分值的类别(左值、右值)
// 如何避免拷贝和移动?
// 使用引用参数
// 左值引用:就无法接收右值参数
// 右值引用:就无法接收左值参数
// 常量引用:万能引用,既可以引用左值、也可以引用右值
// 原因:由于我们写的是常量引用,这样使得传递的参数增加 const 性
// 希望参数“原封不动”传递到目标函数中
// C++ 中支持泛型(模板技术),函数模板而言,很重要的特性,可以实现类型的自动推导
// T
// T& 只能匹配左值
// T&& 可以了
// const T& 不能写,会给参数增加额外的 const 性
template<class T>
Person make_person(T&& param)
{
// 疑问:由于写的是 T&& 看起来是一个右值引用,是不是说,无论传递进来的是左值还是右值,都被转换为右值类型?
// 看看,参数究竟能否正确区分左值和右值
cout << "是否左值:" << is_lvalue_reference<T&&>::value << endl;
cout << "是否右值:" << is_rvalue_reference<T&&>::value << endl;
// T&& 通过引折叠能够确定最终的类型:左值引用、右值引用
// 左值引用:理解为对左值对象的一个别名
// 右值引用:右值是即将被废弃的对象,右值引用目的就是为了给这些即将废弃的对象续命
// 此时,无论左值对象、右值对象都变成具名对象(有名字的对象),是一个左值对象了
// 怎么解决?
// 将 param 再转换回原来的类型,传递到 Person 构造函数里就可以了
// return Person((T&&)param);
// 至此,实现C++参数的完美转发
// std::forward();
// return Person(static_cast<T&&>(param));
// 建议大家使用 forward 函数
return Person(forward<T>(param));
}
void test()
{
// 1. 传递左值
string name = "Obama";
make_person(name);
// 2. 传递右值
make_person(move(name));
}
int main()
{
test();
return 0;
}