大纲 C++ 进阶 万能引用 核心概念 C++ 中的万能引用(universal reference,正式名称是转发引用 forwarding reference)是 C++ 11 引入的一个概念,指在模板参数推导 或 auto 推导 语境下、形如 T&& 或 auto&& 的引用形式;它之所以 “万能”,是因为它既能绑定左值也能绑定右值:当传入左值时 T 被推导为左值引用类型(如 int&),经引用折叠后 T&& 成为左值引用;当传入右值时 T 被推导为非引用类型(如 int),T&& 即为右值引用。判定是否为万能引用的关键有两点:一是必须发生在类型推导语境中(如函数模板参数 T&&、auto&&);二是形式必须严格为 T&&,一旦加上 const(如 const T&&)或出现在非类型推导语境(如指定类模板参数、具体类型,类似 std::vector<T>&& 和 std::vector<int>&&),它就只是普通的右值引用 。万能引用最常见的用途是配合 std::forward<T> 实现完美转发,把参数的左值 / 右值属性原样传递给下层函数,从而避免不必要的拷贝并支持移动语义。需要注意的是,由于万能引用几乎能匹配一切类型,它在重载解析中往往过于贪婪,容易抢走本应匹配其他重载的调用,因此常需配合 std::enable_if、std::is_same 或 Concepts(C++ 20)加以约束。简而言之,万能引用并不是一种新的引用类型 ,而是指在特定模板类型推导场景下的 T&&,它既可以绑定左值,也可以绑定右值,并根据传入实参的值类别(左值或右值)进行类型推导,结合引用折叠实现对不同类型参数的统一处理。
左值引用、右值引用、移动语义、完美转发、引用折叠的介绍
案例代码 右值引用的使用案例 这里的案例代码演示了右值引用的使用,万能引用不会生效 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <iostream> template <typename T>T takeRvalue (int && x) { std::cout << "takeRvalue(int&&) : " << x << std::endl; return x; } int main () { takeRvalue<int >(10 ); int a = 20 ; takeRvalue<int >(25 ); return 0 ; }
程序运行的结果如下:
1 2 takeRvalue(int&&) : 10 takeRvalue(int&&) : 25
万能引用的使用案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <iostream> template <typename T>T takeRvalue (T&& x) { std::cout << "takeRvalue(T&&) : " << x << std::endl; return x; } int main () { takeRvalue<int >(10 ); int a = 20 ; takeRvalue<int &>(a); takeRvalue (a); takeRvalue (25 ); return 0 ; }
程序运行的结果如下:
1 2 3 4 takeRvalue (T&&) : 10 takeRvalue (T&&) : 20 takeRvalue (T&&) : 20 takeRvalue (T&&) : 25
引用折叠的说明
引用折叠只发生在 "引用的引用" 这种形式上,即 & &、& &&、&& &、&& && 这四种组合。只有当 T 本身是引用类型(比如左值推导出 int&)时,T&& 代入后才会形成 int& &&,才需要折叠。如果右值推导出的 T 是 int,代入 T&& 直接就是 int&&,没有嵌套引用,自然不会发生引用折叠。
与右值引用的区别 万能引用与右值引用的区别如下:
本质不同:右值引用是具体类型(如 int&&),万能引用是模板 / auto 类型推导语境下 T&& 的表现形式。 推导条件:万能引用必须发生在类型推导语境(如函数模板参数、auto&&);右值引用出现在具体类型或非类型推导语境。 形式要求:万能引用必须严格是 T&&;如果写成 const T&&、std::vector<int>&& 等就是普通右值引用。 绑定能力:万能引用既能绑定左值也能绑定右值;右值引用只能绑定右值。 推导结果:万能引用传左值推导为 T = int&,经引用折叠得到 int&;传右值推导为 T = int,得到 int&&。右值引用类型固定不变。 用途不同:万能引用主要用于完美转发(配合 std::forward);右值引用主要用于移动语义和移动构造 / 移动赋值。 重载影响:万能引用几乎匹配一切类型,容易在重载中 “抢走” 调用,需用 enable_if / Concepts(C++ 20)约束;右值引用不会如此贪婪。 总结
右值引用是固定类型的右值引用。 万能引用是会随实参推导、既能代表左值引用又能代表右值引用的模板形式。 模板类型推导 核心概念 C++ 模板类型推导是编译器在调用函数模板或实例化类模板时,根据实参(或显式指定的模板实参)自动推导模板参数类型的过程:对函数模板而言,编译器会拿实参类型与形参模式(如 T、T&、const T&、T&& 等)进行匹配,并在匹配中应用一系列规则 —— 按值传参时忽略顶层 const / volatile 并发生数组到指针、函数到指针的退化,按引用传参时保留 cv 限定与数组 / 函数类型本身,对转发引用 T&& 则依据实参是左值还是右值分别推导为左值引用或非引用类型,同时 std::forward、std::move、auto、decltype 等机制与模板推导共享同一套规则;当推导出现歧义或失败(如形参为 T 却传入数组、T 同时出现在多个形参中导致类型冲突)时编译会报错,而显式指定模板实参可以绕过推导;理解这套规则是掌握完美转发、万能引用以及 auto 推导行为的关键。
案例代码 特别注意
以下案例代码都运行在 Windows 环境,并依赖了 Boost 库(主要是为了方便查看模板类型的推导结果,使用的 Boost 库版本是 1.92)。
值类型推导 值类型推导
当模板形参为值类型 T 时,实参的引用属性会被忽略,按照被引用对象的类型进行推导。 实参的顶层 const / volatile 也会被忽略。 例如:int& → T = intconst int → T = intvolatile int → T = int 案例代码一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <boost/type_index.hpp> #include <iostream> template <typename T>void func (T val) { std::cout << "---------------begin---------------" << std::endl; using boost::typeindex::type_id_with_cvr; std::cout << "T = " << type_id_with_cvr<T>().pretty_name () << std::endl; std::cout << "val = " << type_id_with_cvr<decltype (val)>().pretty_name () << std::endl; std::cout << "---------------end---------------" << std::endl; } int main () { int a = 150 ; const int b = a; const int & c = a; char str[] = "I Love C++" ; const char * const ptr = str; func (a); func (b); func (c); func (ptr); return 0 ; }
程序运行的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ---------------begin--------------- T = int val = int ---------------end--------------- ---------------begin--------------- T = int val = int ---------------end--------------- ---------------begin--------------- T = int val = int ---------------end--------------- ---------------begin--------------- T = char const* val = char const* ---------------end---------------
案例代码二
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <boost/type_index.hpp> #include <iostream> template <typename T>void func (const T val) { std::cout << "---------------begin---------------" << std::endl; using boost::typeindex::type_id_with_cvr; std::cout << "T = " << type_id_with_cvr<T>().pretty_name () << std::endl; std::cout << "val = " << type_id_with_cvr<decltype (val)>().pretty_name () << std::endl; std::cout << "---------------end---------------" << std::endl; } int main () { int a = 150 ; const int b = a; const int & c = a; char str[] = "I Love C++" ; const char * const ptr = str; func (a); func (b); func (c); func (ptr); return 0 ; }
程序运行的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ---------------begin--------------- T = int val = int const ---------------end--------------- ---------------begin--------------- T = int val = int const ---------------end--------------- ---------------begin--------------- T = int val = int const ---------------end--------------- ---------------begin--------------- T = char const* val = char const* const ---------------end---------------
引用类型推导 引用类型推导
当模板形参为引用类型 T& 或 const T& 时,实参的引用属性不会简单地按值类型处理,模板推导会根据实参的实际类型进行推导。 引用类型形参不会导致数组、函数发生退化,可以保留数组或函数的原始类型。 例如:int 传给 T& → T = intconst int 传给 const T& → T = intint[5] 传给 T& → T = int[5] 案例代码一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <boost/type_index.hpp> #include <iostream> template <typename T>void func (T& val) { std::cout << "---------------begin---------------" << std::endl; using boost::typeindex::type_id_with_cvr; std::cout << "T = " << type_id_with_cvr<T>().pretty_name () << std::endl; std::cout << "val = " << type_id_with_cvr<decltype (val)>().pretty_name () << std::endl; std::cout << "---------------end---------------" << std::endl; } int main () { int a = 150 ; const int b = a; const int & c = a; func (a); func (b); func (c); return 0 ; }
程序运行的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 ---------------begin--------------- T = int val = int & ---------------end--------------- ---------------begin--------------- T = int const val = int const & ---------------end--------------- ---------------begin--------------- T = int const val = int const & ---------------end---------------
案例代码二
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <boost/type_index.hpp> #include <iostream> template <typename T>void func (const T& val) { std::cout << "---------------begin---------------" << std::endl; using boost::typeindex::type_id_with_cvr; std::cout << "T = " << type_id_with_cvr<T>().pretty_name () << std::endl; std::cout << "val = " << type_id_with_cvr<decltype (val)>().pretty_name () << std::endl; std::cout << "---------------end---------------" << std::endl; } int main () { int a = 150 ; const int b = a; const int & c = a; func (100 ); func (a); func (b); func (c); return 0 ; }
程序运行的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ---------------begin--------------- T = int val = int const & ---------------end--------------- ---------------begin--------------- T = int val = int const & ---------------end--------------- ---------------begin--------------- T = int val = int const & ---------------end--------------- ---------------begin--------------- T = int val = int const & ---------------end---------------
指针类型推导 指针类型推导
当模板形参涉及指针类型时,需要区分顶层 const 和底层 const。 指针本身的顶层 const 在按值推导时会被忽略,而指针所指向对象的底层 const 会保留。 例如:int* const → T = int*const int* → T = const int* 因此,const int* 与 int* const 在模板类型推导中的处理方式不同。 案例代码一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <boost/type_index.hpp> #include <iostream> template <typename T>void func (T* val) { std::cout << "---------------begin---------------" << std::endl; using boost::typeindex::type_id_with_cvr; std::cout << "T = " << type_id_with_cvr<T>().pretty_name () << std::endl; std::cout << "val = " << type_id_with_cvr<decltype (val)>().pretty_name () << std::endl; std::cout << "---------------end---------------" << std::endl; } int main () { int a = 150 ; const int b = a; const int * c = &a; func (&a); func (&b); func (c); return 0 ; }
程序运行的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 ---------------begin--------------- T = int val = int * ---------------end--------------- ---------------begin--------------- T = int const val = int const * ---------------end--------------- ---------------begin--------------- T = int const val = int const * ---------------end---------------
案例代码二
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <boost/type_index.hpp> #include <iostream> template <typename T>void func (const T* val) { std::cout << "---------------begin---------------" << std::endl; using boost::typeindex::type_id_with_cvr; std::cout << "T = " << type_id_with_cvr<T>().pretty_name () << std::endl; std::cout << "val = " << type_id_with_cvr<decltype (val)>().pretty_name () << std::endl; std::cout << "---------------end---------------" << std::endl; } int main () { int a = 150 ; const int b = a; const int * c = &a; func (&a); func (&b); func (c); return 0 ; }
程序运行的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 ---------------begin--------------- T = int val = int const * ---------------end--------------- ---------------begin--------------- T = int val = int const * ---------------end--------------- ---------------begin--------------- T = int val = int const * ---------------end---------------
万能引用推导 万能引用推导
当模板形参为 T&&,且 T 是待推导类型时,它属于万能引用(也叫转发引用)。 推导结果取决于实参的值类别:实参为左值 → T 推导为 T&,经过引用折叠后,形参为左值引用; 实参为右值 → T 推导为非引用类型,形参为右值引用。 这是 C++ 完美转发的基础。 案例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <boost/type_index.hpp> #include <iostream> template <typename T>void func (T&& val) { std::cout << "---------------begin---------------" << std::endl; using boost::typeindex::type_id_with_cvr; std::cout << "T = " << type_id_with_cvr<T>().pretty_name () << std::endl; std::cout << "val = " << type_id_with_cvr<decltype (val)>().pretty_name () << std::endl; std::cout << "---------------end---------------" << std::endl; } int main () { int a = 150 ; const int b = a; const int & c = a; func (a); func (b); func (c); func (100 ); return 0 ; }
程序运行的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ---------------begin--------------- T = int& val = int& ---------------end--------------- ---------------begin--------------- T = int const& val = int const& ---------------end--------------- ---------------begin--------------- T = int const& val = int const& ---------------end--------------- ---------------begin--------------- T = int val = int&& ---------------end---------------
数组做实参传递 数组类型推导
当模板形参为值类型 T 时,数组作为实参会发生数组到指针的退化。 当模板形参为引用类型时,数组不会发生退化,可以保留完整的数组类型。 例如:T 传入 int[5] → T = int*T& 传入 int[5] → T = int[5] 因此,使用 T(&)[N] 可以在模板推导过程中同时保留数组类型并推导出数组大小 N。 案例代码一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <boost/type_index.hpp> #include <iostream> template <typename T>void func (T val) { std::cout << "---------------begin---------------" << std::endl; using boost::typeindex::type_id_with_cvr; std::cout << "T = " << type_id_with_cvr<T>().pretty_name () << std::endl; std::cout << "val = " << type_id_with_cvr<decltype (val)>().pretty_name () << std::endl; std::cout << "---------------end---------------" << std::endl; } int main () { const char str[] = "I Love C++" ; func (str); return 0 ; }
程序运行的结果如下:
1 2 3 4 ---------------begin--------------- T = char const* val = char const* ---------------end---------------
案例代码二
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <boost/type_index.hpp> #include <iostream> template <typename T>void func (T& val) { std::cout << "---------------begin---------------" << std::endl; using boost::typeindex::type_id_with_cvr; std::cout << "T = " << type_id_with_cvr<T>().pretty_name () << std::endl; std::cout << "val = " << type_id_with_cvr<decltype (val)>().pretty_name () << std::endl; std::cout << "---------------end---------------" << std::endl; } int main () { const char str[] = "I Love C++" ; func (str); return 0 ; }
程序运行的结果如下:
1 2 3 4 ---------------begin--------------- T = char const [11] val = char const (&) [11] ---------------end---------------
函数名做实参传递 函数类型推导
当模板形参为值类型 T 时,函数作为实参会发生函数到函数指针的退化。 当模板形参为引用类型时,函数不会发生退化,可以保留函数本身的类型。 例如:T 传入函数 → T 推导为函数指针类型T& 传入函数 → T 推导为函数类型 因此,使用函数引用作为模板形参,可以保留函数本身的类型信息。 案例代码一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <boost/type_index.hpp> #include <iostream> template <typename T>void func (T val) { std::cout << "---------------begin---------------" << std::endl; using boost::typeindex::type_id_with_cvr; std::cout << "T = " << type_id_with_cvr<T>().pretty_name () << std::endl; std::cout << "val = " << type_id_with_cvr<decltype (val)>().pretty_name () << std::endl; std::cout << "---------------end---------------" << std::endl; } void process () { std::cout << "process()" << std::endl; } int main () { func (process); return 0 ; }
程序运行的结果如下:
1 2 3 4 ---------------begin--------------- T = void (__cdecl*)(void) val = void (__cdecl*)(void) ---------------end---------------
案例代码二
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <boost/type_index.hpp> #include <iostream> template <typename T>void func (T& val) { std::cout << "---------------begin---------------" << std::endl; using boost::typeindex::type_id_with_cvr; std::cout << "T = " << type_id_with_cvr<T>().pretty_name () << std::endl; std::cout << "val = " << type_id_with_cvr<decltype (val)>().pretty_name () << std::endl; std::cout << "---------------end---------------" << std::endl; } void process () { std::cout << "process()" << std::endl; } int main () { func (process); return 0 ; }
程序运行的结果如下:
1 2 3 4 ---------------begin--------------- T = void __cdecl(void) val = void (__cdecl&)(void) ---------------end---------------
引用折叠 核心概念 C++ 引用折叠(Reference Collapsing,又叫引用坍塌)是指在模板类型推导、万能引用等场景中,当多个引用类型组合在一起时,编译器会按照特定规则将其折叠为最终的引用类型。引用折叠遵循只要出现左值引用 &,最终结果通常就是左值引用;只有两个都是右值引用 && 时,结果才是右值引用 的规则;即 & + &、& + &&、&& + & 最终都折叠为 &(左值引用),只有 && + && 折叠为 &&(右值引用)。引用折叠是 C++ 万能引用、完美转发以及模板类型推导等机制能够正常工作的基础。
组合方式 折叠结果 说明 & + &&左值引用 + 左值引用 → 左值引用 & + &&&左值引用 + 右值引用 → 左值引用 && + &&右值引用 + 左值引用 → 左值引用 && + &&&&右值引用 + 右值引用 → 右值引用
案例代码 特别注意
以下案例代码运行在 Windows 环境,并依赖了 Boost 库(主要是为了方便查看模板类型推导的结果)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <boost/type_index.hpp> #include <iostream> template <typename T>void func (T&& val) { std::cout << "---------------begin---------------" << std::endl; using boost::typeindex::type_id_with_cvr; std::cout << "T = " << type_id_with_cvr<T>().pretty_name () << std::endl; std::cout << "val = " << type_id_with_cvr<decltype (val)>().pretty_name () << std::endl; std::cout << "---------------end---------------" << std::endl; } int main () { int a = 150 ; func (a); func (100 ); return 0 ; }
程序运行的结果如下:
1 2 3 4 5 6 7 8 ---------------begin--------------- T = int& val = int& ---------------end--------------- ---------------begin--------------- T = int val = int&& ---------------end---------------
案例分析 在上述案例代码中:
当模板形参为 T&&,且 T 是待推导类型时,它属于万能引用(也叫转发引用)。 推导结果取决于实参的值类别:实参为左值 → T 推导为 T&,经过引用折叠后,形参最终为左值引用; 实参为右值 → T 推导为非引用类型,形参最终为右值引用。 这是 C++ 完美转发的基础。 转发 核心概念 C++ 转发(Forwarding)是指在函数调用过程中,将函数参数继续传递给其他函数,并尽可能保持参数原有的值类别(左值或右值) 以及类型信息。转发主要与引用折叠、万能引用和 std::forward 配合使用,其中完美转发 可以使传递进来的左值仍以左值形式传递,右值仍以右值形式传递,从而避免不必要的拷贝并正确调用对应的重载函数。转发机制是 C++ 泛型编程中实现高效、通用参数传递的重要基础。
案例代码 案例代码一(验证值传递)
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <iostream> void func (int i, int j) { ++i; std::cout << i + j << std::endl; } int main () { int a = 10 ; func (a, 30 ); std::cout << a << std::endl; return 0 ; }
程序运行的结果如下:
案例代码二(验证引用传递)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <iostream> void func (int & i, int j) { ++i; std::cout << i + j << std::endl; } int main () { int a = 10 ; func (a, 30 ); std::cout << a << std::endl; return 0 ; }
程序运行的结果如下:
案例代码三(验证转发)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <iostream> template <typename F, typename T1, typename T2>void process (F f, T1 t1, T2 t2) { f (t1, t2); } void func (int & i, int j) { ++i; std::cout << i + j << std::endl; } int main () { int a = 10 ; process (func, a, 30 ); std::cout << a << std::endl; return 0 ; }
程序运行的结果如下:
代码分析
在上述案例代码中,process() 函数模板实例化后,其参数类型相当于 void process(void (*f)(int &, int), int t1, int t2)。调用 process(func, a, 30) 时,实参 a 被传递给 process() 的形参 t1,由于 t1 是 int 类型,因此这里发生了一次值拷贝,t1 与 a 是两个独立的 int 对象。随后调用 func(t1, t2) 时,虽然 func() 的第一个参数是 int&,但它引用的是 process() 中的局部变量 t1,而不是外部的变量 a,因此 func() 对第一个参数的修改不会影响 a 的值。
案例代码四(验证转发)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <iostream> template <typename F, typename T1, typename T2>void process (F f, T1&& t1, T2 t2) { f (t1, t2); } void func (int & i, int j) { ++i; std::cout << i + j << std::endl; } int main () { int a = 10 ; process (func, a, 30 ); std::cout << a << std::endl; return 0 ; }
程序运行的结果如下:
代码分析
在上述案例代码中,process() 函数模板实例化后,其参数类型相当于 void process(void (*f)(int &, int), int& t1, int t2)。调用 process(func, a, 30) 时,由于 t1 是万能引用,传入左值 a 后,根据万能引用的模板参数类型推导规则,T1 被推导为 int&,经过引用折叠后,T1&& 最终变为 int&(即左值引用),因此 t1 实际上直接引用了变量 a,没有产生额外的拷贝。随后调用 func(t1, t2) 时,func() 的第一个参数 int& 绑定到 t1 所引用的 a,所以 func() 中对 i 的修改最终会改变变量 a 的值。
案例代码五(验证转发)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <iostream> template <typename F, typename T1, typename T2>void process (F f, T1&& t1, T2 t2) { f (t1, t2); } void func (int && i, int j) { ++i; std::cout << i + j << std::endl; } int main () { int a = 10 ; process (func, a, 30 ); std::cout << a << std::endl; return 0 ; }
代码分析
上述案例代码会编译失败 ,process() 函数模板实例化后,其参数类型相当于 void process(void (*f)(int&&, int), int& t1, int t2)。调用 process(func, a, 30) 时,由于 a 是左值,万能引用 T1&& 会将 T1 推导为 int&,经过引用折叠后,T1&& 最终变为 int&,因此 t1 实际上是一个左值引用。另外,func() 的第一个参数要求的是 int&&,只能绑定到右值,但在 process() 函数内部,即使 t1 的类型是 int&,表达式 t1 本身仍然是一个左值 ,因此调用 f(t1, t2) 时无法将这个左值 t1 绑定到 func() 的 int&& i 参数,从而产生 cannot bind rvalue reference of type 'int&&' to lvalue of type 'int' 的编译错误。解决该编译错误的方法是引入 C++ 提供的完美转发机制,因为完美转发可以使传递进来的左值仍以左值形式传递,右值仍以右值形式传递 。
完美转发 核心概念 C++ 完美转发(Perfect Forwarding)是指在函数模板中,将接收到的参数继续传递给其他函数时,能够完整保留参数原有的类型信息 和值类别(左值或右值) 。也就是说,传入左值时仍以左值形式传递,传入右值时仍以右值形式传递,从而避免不必要的拷贝或移动,并保证调用到正确的函数重载。完美转发通常依赖于万能引用、引用折叠以及 std::forward 等机制,是现代 C++ 泛型编程和高性能库设计中的重要基础。
实现方式 C++ 的完美转发通常是通过万能引用(转发引用)结合 std::forward 实现,其中万能引用负责保留模板参数的类型信息,std::forward 负责在转发时恢复模板参数原有的值类别(左值或右值),从而实现完美转发。除此之外,完美转发还有一些特殊情况下的替代实现方式:
(1) 重载函数:分别提供左值和右值版本,可以保持值类别,但需要手动编写多个重载,通用性较差。 (2) 显式指定模板参数:可以通过模板参数控制最终的参数类型,但使用起来不够灵活,也不能像转发引用那样自动推导。 (3) std::move 移动语义:可以将参数统一转换为右值,但它不是完美转发,因为它无法保留左值的原始属性。 (4) 直接传递引用:如果明确知道参数只需要以引用形式传递,可以使用 T& 或 const T&,但同样不能同时保留左值和右值的值类别。 值得一提的是,C++ 中的 std::forward 是用于实现完美转发的工具之一,它能够根据模板参数的类型推导结果,在转发参数时尽可能保持参数原本的值类别(左值或右值),也就是说,传入左值时仍以左值形式转发,传入右值时仍以右值形式转发。由于函数形参在函数体内部即使声明为右值引用,其表达式本身也会被视为左值,因此仅依靠万能引用无法直接保持原有的值类别,而 std::forward 可以结合引用折叠规则恢复参数原本的左值或右值属性,从而避免不必要的拷贝,并确保调用正确的重载函数 。
案例代码 使用万能引用 + std::forward 实现完美转发 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <iostream> template <typename F, typename T1, typename T2>void process (F f, T1&& t1, T2 t2) { f (std::forward<T1>(t1), t2); } void func (int && i, int j) { ++i; std::cout << i + j << std::endl; } int main () { int a = 10 ; process (func, std::move (a), 30 ); std::cout << a << std::endl; return 0 ; }
程序运行的结果如下:
代码分析
在上述案例代码中,调用 process(func, std::move(a), 30) 时,由于 std::move(a) 将变量 a 转换为右值,因此万能引用 T1&& 会将 T1 推导为 int 类型,此时 T1&& 直接为 int&&。虽然 t1 的类型为 int&&,但由于 t1 是一个有名字的变量,在函数体中使用 t1 时,其表达式 仍然是左值,因此直接调用 f(t1, t2) 会编译失败。通过 std::forward<T1>(t1) 可以根据 T1 的推导结果恢复参数原有的值类别,由于 T1 为 int,因此会将 t1 以右值形式(int&&)转发给 func(),从而成功绑定到 func() 的 int&& 参数。func() 中对 i 的修改实际上修改的是原来的变量 a,因此最终 a 的值变为 11。
与 move 的区别 std::move 与 std::forward 的主要区别
核心作用不同std::move:将表达式无条件转换为右值,用于触发移动语义。std::forward:根据模板参数推导结果,有条件地恢复参数原有的值类别,用于实现完美转发。 是否保留原有值类别std::move:不保留原有的值类别,无论原来是左值还是右值,都会转换为右值。std::forward:保留原有的值类别,原来是左值则转发为左值,原来是右值则转发为右值。 对模板参数的依赖std::move:通常不需要依赖参数原本的值类别。std::forward:通常需要结合万能引用和模板类型推导,根据推导出的类型决定最终的值类别。 典型使用场景std::move:对象明确不再需要保持原有值,准备将其资源移动出去时使用。std::forward:函数模板继续传递参数时使用,尤其适用于完美转发。 核心区别总结
std::move 可以理解为:"不管原来是什么值类别(左值或右值),都将它转换为右值"。std::forward 可以理解为:"原来是什么值类别(左值或右值),转发时就尽量保持什么值类别"。std::move 与 std::forward 的案例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <iostream> void printInfo (int & val) { std::cout << "printInfo(int&)" << std::endl; } void printInfo (int && val) { std::cout << "printInfo(int&&)" << std::endl; } template <typename T>void process (T&& t) { std::cout << "---------------begin---------------" << std::endl; printInfo (t); printInfo (std::forward<T>(t)); printInfo (std::move (t)); std::cout << "---------------end---------------" << std::endl; } int main () { int a = 5 ; process (a); process (10 ); return 0 ; }
程序运行的结果如下:
1 2 3 4 5 6 7 8 9 10 ---------------begin--------------- printInfo(int&) printInfo(int&) printInfo(int&&) ---------------end--------------- ---------------begin--------------- printInfo(int&) printInfo(int&&) printInfo(int&&) ---------------end---------------
代码分析
在上述案例代码中,当 process() 传入左值 a 时,T 被推导为 int&,T&& 经过引用折叠后为 int&;传入右值 10 时,T 被推导为 int,T&& 为 int&&。在函数内部,虽然 t 在右值情况下的类型为 int&&,但由于 t 是有名字的变量,其表达式仍然是左值,因此直接调用 printInfo(t) 始终匹配 int& 版本;std::forward<T>(t) 会根据 T 的推导结果保持原始参数的值类别,因此左值调用 int&、右值调用 int&&;而 std::move(t) 会无条件将 t 转换为右值,因此两种情况下都调用 int&& 版本。