参数转发(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; }
是否左值:1 是否右值:0 左值 --------- 是否左值:0 是否右值:1 左值
引用折叠是 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; }
2024/06/10更新
最后完美转发一定要强转成T&&吗,强转成T会有什么缺陷吗?我实测强转成T传入构造函数,也能达到一样的效果
如果写 T 的话,类型转化会发生拷贝行为。
很有帮助👍