C++ 从入门到精通之十八

大纲

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>

// 模板参数 T 只用于返回类型,形参是具体类型的右值引用
template <typename T>
T takeRvalue(int&& x) {
std::cout << "takeRvalue(int&&) : " << x << std::endl;
return x;
}

int main() {
// 传右值,正常调用;右值引用生效(int&& 绑定右值)
takeRvalue<int>(10);

int a = 20;

// 传左值,编译失败:int&& 是右值引用,不能绑定左值;右值引用生效(拒绝左值)
// takeRvalue<int>(a);

// 传右值,正常调用;右值引用生效(int&& 绑定右值)
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>

// 模板参数 T 参与形参推导,形参 T&& 是万能引用
template <typename T>
T takeRvalue(T&& x) {
std::cout << "takeRvalue(T&&) : " << x << std::endl;
return x;
}

int main() {
// 显式指定 T=int,形参为 int&&,绑定右值;万能引用未生效(表现为普通右值引用)
takeRvalue<int>(10);

int a = 20;

// 显式指定 T=int,形参是 int&&,左值 a 不能绑定到 int&&,会编译失败;也就是说,显式指定 T 后它表现得像普通右值引用,万能引用未生效
// takeRvalue<int>(a);

// 显式指定 T=int&,形参折叠为 int&,绑定左值;万能引用未生效(表现为普通左值引用)
takeRvalue<int&>(a);

// 不显式指定 T,传左值:T 推导为 int&,形参 T&& 代入得 int& &&,经引用折叠为 int&,绑定左值;万能引用生效
takeRvalue(a);

// 不显式指定 T,传右值:T 推导为 int,形参 T&& 代入即 int&&,无需折叠,绑定右值;万能引用生效
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 = int
    • const int → T = int
    • volatile 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;
// 查看模板参数 T 的类型推导结果
std::cout << "T = " << type_id_with_cvr<T>().pretty_name() << std::endl;
// 查看形参 val 的类型推导结果
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;
// 查看模板参数 T 的类型推导结果
std::cout << "T = " << type_id_with_cvr<T>().pretty_name() << std::endl;
// 查看形参 val 的类型推导结果
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 = int
    • const int 传给 const T& → T = int
    • int[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;
// 查看模板参数 T 的类型推导结果
std::cout << "T = " << type_id_with_cvr<T>().pretty_name() << std::endl;
// 查看形参 val 的类型推导结果
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;
// 查看模板参数 T 的类型推导结果
std::cout << "T = " << type_id_with_cvr<T>().pretty_name() << std::endl;
// 查看形参 val 的类型推导结果
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;
// 查看模板参数 T 的类型推导结果
std::cout << "T = " << type_id_with_cvr<T>().pretty_name() << std::endl;
// 查看形参 val 的类型推导结果
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;
// 查看模板参数 T 的类型推导结果
std::cout << "T = " << type_id_with_cvr<T>().pretty_name() << std::endl;
// 查看形参 val 的类型推导结果
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;
// 查看模板参数 T 的类型推导结果
std::cout << "T = " << type_id_with_cvr<T>().pretty_name() << std::endl;
// 查看形参 val 的类型推导结果
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;
// 查看模板参数 T 的类型推导结果
std::cout << "T = " << type_id_with_cvr<T>().pretty_name() << std::endl;
// 查看形参 val 的类型推导结果
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;
// 查看模板参数 T 的类型推导结果
std::cout << "T = " << type_id_with_cvr<T>().pretty_name() << std::endl;
// 查看形参 val 的类型推导结果
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;
// 查看模板参数 T 的类型推导结果
std::cout << "T = " << type_id_with_cvr<T>().pretty_name() << std::endl;
// 查看形参 val 的类型推导结果
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;
// 查看模板参数 T 的类型推导结果
std::cout << "T = " << type_id_with_cvr<T>().pretty_name() << std::endl;
// 查看形参 val 的类型推导结果
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;
// 查看模板参数 T 的类型推导结果
std::cout << "T = " << type_id_with_cvr<T>().pretty_name() << std::endl;
// 查看形参 val 的类型推导结果
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); // 实参传递值(左值),会发生引用折叠,模板参数 T 推导为 int& 类型,形参 val 推导为 int& 类型(左值引用)
func(100); // 实参传递值(右值),不会发生引用折叠,模板参数 T 推导为 int 类型,形参 val 推导为 int&& 类型(右值引用)
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; // 改变 i 的值,不会影响外面的值
std::cout << i + j << std::endl;
}

int main() {
int a = 10;
func(a, 30);
std::cout << a << std::endl; // 注意,变量 a 的值不会发生改变,因为是值传递
return 0;
}

程序运行的结果如下:

1
2
41
10

案例代码二(验证引用传递)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

// 第一个参数是引用
void func(int& i, int j) {
++i; // 改变 i 的值,会影响外面的值
std::cout << i + j << std::endl;
}

int main() {
int a = 10;
func(a, 30);
std::cout << a << std::endl; // 注意,变量 a 的值会发生改变,因为是引用传递
return 0;
}

程序运行的结果如下:

1
2
41
11

案例代码三(验证转发)

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 是要调用的第三方函数,也就是要转发到的目标函数
f(t1, t2);
}

// 目标函数,第一个参数是左值引用
void func(int& i, int j) {
++i; // 改变 i 的值
std::cout << i + j << std::endl;
}

int main() {
int a = 10;
process(func, a, 30); // 通过 process() 间接调用 func() 函数
std::cout << a << std::endl; // 注意,变量 a 的值不会发生改变,即使 func() 函数的第一个参数是引用类型
return 0;
}

程序运行的结果如下:

1
2
41
10

代码分析

在上述案例代码中,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 是要调用的第三方函数,也就是要转发到的目标函数
f(t1, t2);
}

// 目标函数,第一个参数是左值引用
void func(int& i, int j) {
++i; // 改变 i 的值
std::cout << i + j << std::endl;
}

int main() {
int a = 10;
process(func, a, 30); // 通过 process() 间接调用 func() 函数
std::cout << a << std::endl; // 注意,变量 a 的值会发生改变
return 0;
}

程序运行的结果如下:

1
2
41
11

代码分析

在上述案例代码中,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 是要调用的第三方函数,也就是要转发到的目标函数
f(t1, t2);
}

// 目标函数,第一个参数是右值引用
void func(int&& i, int j) {
++i; // 改变 i 的值
std::cout << i + j << std::endl;
}

int main() {
int a = 10;
process(func, a, 30); // 编译失败,通过 process() 间接调用 func() 函数
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 是要调用的第三方函数,也就是要转发到的目标函数
// 由于 process() 传递进来的第一个参数是右值,因此 std::forward 会将表达式 t1 从左值转换为右值,然后再传递给 func()
f(std::forward<T1>(t1), t2);
}

// 目标函数,第一个参数是右值引用
void func(int&& i, int j) {
++i; // 改变 i 的值
std::cout << i + j << std::endl;
}

int main() {
int a = 10;
process(func, std::move(a), 30); // 通过 process() 间接调用 func() 函数,并使用 std::move 将变量 a 转换为右值
std::cout << a << std::endl; // 注意,变量 a 的值会发生改变
return 0;
}

程序运行的结果如下:

1
2
41
11

代码分析

在上述案例代码中,调用 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); // 表达式 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&& 版本。