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

大纲

C++ 进阶

引用折叠

核心概念

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&& 版本。

auto 类型推导

核心概念

C++ 中的 auto 是一种类型推导关键字,编译器会根据变量初始化表达式自动推导出变量的实际类型,从而减少冗长的类型声明。这里 auto 的类型推导规则与模板类型推导较为类似,会根据初始化表达式的类型、值类别(左值或右值)以及是否存在引用、const 等限定符进行推导,但 auto 本身并不是一种具体的数据类型;需要注意的是,使用 auto 声明变量时通常必须进行初始化。它常用于 STL 迭代器、复杂模板类型以及泛型编程场景,可以提高代码的简洁性和可读性,但在类型不够直观时也可能降低代码的可读性。auto 的使用特点如下:

  • (1) 不影响运行性能:auto 自动类型推导发生在编译期,因此不会影响程序执行期间的性能。
  • (2) 必须立即初始化:定义 auto 变量时必须立即进行初始化,这样编译器才能推导出它的实际类型。
  • (3) 编译期类型替换:编译器在代码编译时确定 auto 的类型和整个变量的类型,然后用真正的类型替换掉 auto 这个类型占位符。
  • (4) 使用灵活且结合性强:auto 的使用非常灵活,可以和指针、引用以及 const 等限定符结合使用。

适用场景

  • auto 的适用场景

    • 迭代器声明:类型名冗长(如 std::vector<int>::iterator),用 auto 更简洁清晰。
    • 范围 for 循环:遍历容器时用 auto& 或 const auto& 避免拷贝并自动匹配元素类型。
    • Lambda 表达式:闭包类型无法显式写出,只能用 auto 或模板类型推导。
    • 函数返回类型推导(C++ 14 起支持):简化返回类型书写,auto 按值、auto& 保留引用、decltype(auto) 完整保留。
    • 模板编程:依赖类型无法手写,必须借助 auto。
    • 类型名冗长或难以书写:如嵌套模板、std::map 的值类型、std::function 等。
    • 避免隐式类型转换:用 auto 保持表达式的精确类型,减少意外的隐式类型转换。
    • 结构化绑定配合(C++ 17 起支持):auto [a, b] = pair; 解包元组、结构体。
    • 函数指针 / 成员指针:简化声明,减少出错。
    • 需要显式控制 const / 引用语义时:const auto&、auto&& 等组合灵活表达意图。
  • auto 的不适用场景

    • 需要隐式类型转换时:auto 会保持精确类型,不会做你想要的转换(如 float f = 1.0; 用 auto 会得到 double)。
    • 代理类 / 表达式模板场景:auto 可能推导出代理类型而非实际值类型(如 std::vector<bool>::reference),导致悬垂引用或意外行为。
    • 需要明确指定类型时:如 int 与 long 的溢出边界、unsigned 与 signed 的语义差异,显式写出类型更安全。
    • 函数参数:C++ 20 之前,普通函数参数不能使用 auto(C++ 20 起可用作简写模板)。
    • 类成员变量:非静态成员变量不能用 auto 推导(静态成员变量可以使用)。
    • 数组声明:auto arr[3] = {1, 2, 3}; 不合法,不能推导出数组类型。
    • 需要显式 const 时:auto 会丢弃顶层的 const,必须自己写 const auto。
    • 初始化列表推导:auto x = {1, 2, 3}; 会推导成 std::initializer_list<int>,多数场景并非本意,应显式写容器类型。
    • 返回引用但希望调用方明确感知时:auto 按值返回会丢引用,需用 auto& 或 decltype(auto)。
    • 可读性优先的公共接口:库的公开 API 显式写出类型更利于文档和跨版本稳定。
    • C 语义风格数组退化场景:auto 会将数组退化为指针,丢失数组长度信息。
    • 位域:不能对位域使用 auto& 绑定。

推导规则

特别注意

以下案例代码都运行在 Windows 环境,并依赖了 Boost 库(主要是为了方便查看 auto 类型推导的结果,使用的 Boost 库版本是 1.92)。

常规类型推导
推导规则
  • 常规类型推导规则二:传指针或引用类型方式(但不是万能引用)

    • 当 auto 带 & 或 * 时,根据指针或引用声明形式进行类型推导。
    • 要点:
      • 当 auto 带 & 时,推导过程中会去除初始化表达式本身的引用,再进行匹配;
      • const 等限定符是否保留,取决于声明形式及其修饰的对象;
      • 对于指针,指针本身的顶层 const 与指向对象的底层 const 需要区别对待。
    • 举例:
      • const int& rx = x; auto& a6 = rx; → auto = const int,a6 为 const int&
      • const int* p = &x; auto* q = p; → auto = const int,q 为 const int*
  • 常规类型推导规则二:传指针或引用类型方式(但不是万能引用)

    • 传指针或引用类型方式(但不是万能引用)不会丢弃 const 等限定符,但是会丢弃引用。
    • 要点:
      • 当 auto 带 & 或 * 时,采用的是引用 / 指针推导规则;
      • 根据指针或引用声明形式推导,保留被引用对象或被指向对象的 const 等限定符;顶层 const 的处理取决于声明形式。
      • 引用会被丢弃(针对 “被推导对象本身是引用” 的情况,即先剥掉引用再匹配)。
    • 举例:
      • const int& rx = x; auto& a6 = rx; → auto = const int,a6 为 const int&
      • const int* p = &x; auto* q = p; → auto = const int,q 为 const int*
  • 常规类型推导规则三:万能引用方式

    • auto&& 遇到左值时,auto 推导为左值引用;遇到右值时,auto 通常推导为非引用类型。初始化表达式的 const 等限定符会根据推导规则保留。
    • 只有当形式严格为 auto&&(或模板中的 T&&,且 T 是被推导的类型)时,才是真正的万能引用;const auto&&、std::vector<int>&& 等都不是万能引用。
    • 推导分两步:
      • (1) 根据初始化表达式的值类别推导 auto:
        • 初始化是左值 → auto = T&(保留 const 等限定符)
        • 初始化是右值 → auto = T(保留 const 等限定符)
      • (2) 代入 auto&&,必要时进行引用折叠:
        • T& && → T&,发生引用折叠
        • T && → T&&,不发生引用折叠
    • 举例:
      • int x = 10; auto&& a = x; → auto = int&,经引用折叠后 a 为 int&
      • const int cx = 10; auto&& b = cx; → auto = const int&,经引用折叠后 b 为 const int&
      • auto&& c = 100; → auto = int,c 为 int&&
      • const int cx = 10; auto&& d = std::move(cx); → auto = const int,d 为 const int&&
  • 常规类型推导规则总结:

    声明形式最终类型是否为引用是否丢弃 constauto 的推导结果变量的最终类型说明
    const int num = 10; auto x = num;❌ 否✅ 丢弃顶层 constintint传值,拷贝一份
    const int num = 10; const auto x = num;❌ 否✅ 推导时丢弃intconst intconst 由开发者显式添加
    const int num = 10; auto& x = num;✅ 是❌ 不丢弃const intconst int&引用推导,保留对象的 const 属性
    const int num = 10; const auto& x = num;✅ 是❌ 不丢弃const intconst int&const 引用绑定 const 对象
    const int num = 10; auto* x = &num;—❌ 不丢弃const intconst int*指针推导,保留被指向对象的 const 属性
    const int num = 10; const auto* x = &num;—❌ 不丢弃const intconst int*指针指向 const 对象
    const int num = 10; auto&& x = num;✅ 是❌ 不丢弃const int&const int&万能引用,左值触发引用折叠

`auto` 常规类型推导的记忆口诀

  • 纯 auto(传值方式):丢弃引用和顶层的 const / volatile,要保留 const 就自己加。
  • auto&(引用)/ auto*(指针):根据声明形式推导,保留被引用对象或被指向对象的 const 等限定符。
  • auto&&(万能引用):绑定左值时推导为左值引用,绑定右值时通常推导为右值引用;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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <boost/type_index.hpp>
#include <iostream>

// 辅助打印推导类型(保留 const/volatile/引用 属性)
template <typename T>
void print_type(const char* name) {
std::cout << name << " = " << boost::typeindex::type_id_with_cvr<T>().pretty_name() << std::endl;
}

int main() {
int x = 10;
const int cx = x;
const int& rx = x;
const auto& qx = x;

// 案例 1:auto 推导基础类型
auto a1 = x; // auto = int
print_type<decltype(a1)>("auto a1 = x");

// 案例 2:auto 推导 const 变量(顶层 const 被丢弃)
auto a2 = cx; // auto = int(const 被忽略)
print_type<decltype(a2)>("auto a2 = cx");

// 案例 3:auto 推导 const 引用(引用和 const 全被丢弃)
auto a3 = rx; // auto = int(引用和 const 均被忽略)
print_type<decltype(a3)>("auto a3 = rx");

// 案例 4:显式加 const,保留 const 属性
const auto a4 = x; // auto = int,a4 类型为 const int
print_type<decltype(a4)>("const auto a4 = x");

// 案例 5:显式加引用,保留引用属性(const 依然保留)
const auto& a5 = x; // auto = int,a5 类型为 const int&
print_type<decltype(a5)>("const auto& a5 = x");

// 案例 6:auto& 推导 const 引用(const 被保留)
auto& a6 = rx; // auto = const int,a6 类型为 const int&
print_type<decltype(a6)>("auto& a6 = rx");

// 案例 7:const auto& 本身是 const 引用,用 auto(非引用)推导时
auto a7 = qx; // auto = int(引用和 const 均被忽略),a7 类型为 int
print_type<decltype(a7)>("auto a7 = qx");

// 案例 8:auto& 推导 const 引用
auto& a8 = qx; // auto = const int(const 被保留),a8 类型为 const int&
print_type<decltype(a8)>("auto& a8 = qx");

// 案例 9:auto 用于 new 表达式
auto a9 = new auto(100); // new 后的 auto 根据初始值 100 被推导为 int,new 表达式返回 int*,故 a9 类型为 int*
print_type<decltype(a9)>("auto a9 = new auto(100)");

// 案例 10:const auto* 指针推导
const auto* a10 = &x; // auto = int,a10 类型为 const int*
print_type<decltype(a10)>("const auto* a10 = &x");

// 案例 11:auto&& 万能引用绑定左值
auto&& a11 = x; // x 是 int 类型的左值,auto&& 遇到左值时,auto 被推导为 int&,再经过引用折叠 int& && 变成 int&,故 a11 类型为 int&
print_type<decltype(a11)>("auto&& a11 = x");

// 案例 12:auto&& 万能引用绑定 const 左值
auto&& a12 = cx; // cx 类型为 const int,是 const 左值,auto&& 遇到左值时 auto 推导为 const int&,再经过引用折叠 const int& && 变成 const int&,故 a12 类型为 const int&
print_type<decltype(a12)>("auto&& a12 = cx");

// 案例 13:auto&& 绑定右值(纯右值)
auto&& a13 = 100; // 100 是 int 类型的纯右值,auto&& 遇到右值时 auto 被推导为 int,代入 auto&& 得到 int&&,无需经过引用折叠,故 a13 类型为 int&&
print_type<decltype(a13)>("auto&& a13 = 100");

return 0;
}

程序运行的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
auto a1 = x = int
auto a2 = cx = int
auto a3 = rx = int
const auto a4 = x = int const
const auto& a5 = x = int const&
auto& a6 = rx = int const&
auto a7 = qx = int
auto& a8 = qx = int const&
auto a9 = new auto(100) = int*
const auto* a10 = &x = int const*
auto&& a11 = x = int&
auto&& a12 = cx = int const&
auto&& a13 = 100 = int&&
数组类型推导
推导规则
  • 数组类型推导规则的要点

    • 传值(无 & 或 &&):数组一律退化为指针,数组长度信息丢失。
    • 引用(& 或 && 绑定左值数组):数组不退化为指针,保留 T[N] 类型和长度。
    • 元素 const 保留:const T[N] 传值退化为 const T*(底层 const 不丢弃)。
    • 数组退化与元素类型无关:char[N]、int[N]、自定义类型数组的退化规则一致。
    • 退化是数组到指针的标准转换,与 auto 本身无关,函数参数按值传递时同样发生。
  • 数组类型推导规则的总结

    声明形式数组是否退化为指针auto 的推导结果变量的最终类型说明
    char arr[] = "I Love C++"; auto x = arr;✅ 退化为指针char*char*传值推导,数组退化为指向首元素的指针
    char arr[] = "I Love C++"; const auto x = arr;✅ 退化为指针char*(const 是开发者显式加的)char* const顶层 const 修饰指针本身
    char arr[] = "I Love C++"; auto& x = arr;❌ 不退化char[11]char(&)[11]引用绑定数组,保留数组类型和长度
    char arr[] = "I Love C++"; const auto& x = arr;❌ 不退化char[11]const char(&)[11]const 修饰数组元素,不能通过引用修改元素
    char arr[] = "I Love C++"; auto&& x = arr;❌ 不退化char(&)[11]char(&)[11](发生引用折叠)数组是左值,触发万能引用推导和引用折叠
    char arr[] = "I Love C++"; auto* x = arr;✅ 退化为指针charchar*根据 auto* 声明形式推导,最终得到 char*
推导案例
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <boost/type_index.hpp>
#include <iostream>

// 辅助打印推导类型(保留 const/volatile/引用 属性)
template <typename T>
void print_type(const char* name) {
std::cout << name << " = " << boost::typeindex::type_id_with_cvr<T>().pretty_name() << std::endl;
}

int main() {
char arr1[] = "I Love C";
const char arr2[] = "I Love C++";
int arr3[] = {1, 2, 3};

// 案例 1:auto 推导数组(传值方式),数组退化为指针
auto str1 = arr1; // arr1 类型为 char[9],按值推导时数组退化为 char*,故 auto = char*,str1 类型为 char*
print_type<decltype(str1)>("auto str1 = arr1");

// 案例 2:auto& 推导数组(引用方式),保留数组类型
auto& str2 = arr1; // arr1 类型为 char[9],auto& 绑定到数组本身,auto = char[9],str2 类型为 char(&)[9]
print_type<decltype(str2)>("auto str2 = arr1");

// 案例 3:auto 推导 const 数组(传值方式),数组退化为指针,元素的 const 属性保留
auto str3 = arr2; // arr2 类型为 const char[11],数组退化为指向首元素的指针,auto 推导为 const char*,str3 类型为 const char*
print_type<decltype(str3)>("auto str3 = arr2");

// 案例 4:auto& 推导 const 数组(引用方式),保留数组类型和 const
auto& str4 = arr2; // arr2 类型为 const char[11],auto& 绑定到数组本身,auto = const char[11],str4 类型为 const char(&)[11]
print_type<decltype(str4)>("auto str4 = arr2");

// 案例 5:auto 推导非字符数组(传值方式),同样会退化为指针
auto str5 = arr3; // arr3 类型为 int[3],按值推导时数组退化为 int*,故 auto = int*,str5 类型为 int*
print_type<decltype(str5)>("auto str5 = arr3");

// 案例 6:auto&&(万能引用)推导非 const 左值数组
auto&& str6 = arr1; // arr1 类型为 char[9],是左值;auto&& 遇到左值时 auto 推导为 char(&)[9],与 && 发生引用折叠后仍为 char(&)[9],故 str6 类型为 char(&)[9]
print_type<decltype(str6)>("auto str6 = arr1");

// 案例 7:auto&&(万能引用)推导 const 左值数组
auto&& str7 = arr2; // arr2 类型为 const char[11],是左值;auto&& 遇到左值时 auto 推导为 const char(&)[11],与 && 发生引用折叠后仍为 const char(&)[11],故 str7 类型为 const char(&)[11]
print_type<decltype(str7)>("auto str7 = arr2");

// 案例 8:auto* 推导数组,根据声明形式推导出指针类型
auto* str8 = arr1; // arr1 类型为 char[9],根据 auto* 的声明形式,auto 推导为 char,str8 类型为 char*
print_type<decltype(str8)>("auto str8 = arr1");

return 0;
}

程序运行的结果如下:

1
2
3
4
5
6
7
8
auto str1 = arr1 = char*
auto str2 = arr1 = char (&) [9]
auto str3 = arr2 = char const*
auto str4 = arr2 = char const (&) [11]
auto str5 = arr3 = int*
auto str6 = arr1 = char (&) [9]
auto str7 = arr2 = char const (&) [11]
auto str8 = arr1 = char*
函数类型推导
推导规则
  • 函数类型推导规则的要点:

    • 函数名传值会退化为函数指针:auto fp = add; 中,函数名 add 在初始化过程中发生函数到指针的标准转换,auto 最终推导为函数指针类型 int(*)(int, int),和数组退化类似。
    • 引用绑定不退化函数指针:auto& fp = add; 中,auto 推导为函数类型 int(int, int),最终 fp 的类型为函数左值引用 int(&)(int, int)。
    • auto* 推导函数指针:auto* fp = add; 中,根据声明形式,auto 推导为函数类型 int(int, int),最终变量 fp 的类型为 int(*)(int, int)。
    • Lambda 是闭包类型:auto lam = lambda; 会推导为唯一的匿名闭包类型,不是函数指针;只有无捕获的 Lambda 才能隐式转为函数指针。
    • 返回类型推导:函数返回 auto 走模板类型推导规则(按值传递时,丢弃引用和顶层 const);auto& 保留引用;decltype(auto) 完整保留。
    • 函数退化与数组退化类比:
      • 数组 T[N]:
        • 传值 auto:退化为 T*
        • 引用 auto&:保留 T(&)[N]
      • 函数 R(Args...):
        • 传值 auto:退化为 R(*)(Args...)
        • 引用 auto&:保留 R(&)(Args...)
      • 两者在传值时都会退化为指针,在引用绑定时都保留原类型,这是 C++ 中 “数组到指针” 和 “函数到指针” 两大标准转换的体现。
  • 函数类型推导规则的总结:

    声明形式是否退化为函数指针auto 的推导结果变量的最终类型说明
    int add(int a, int b) { return a + b; } auto fp = add;✅ 退化为函数指针int(*)(int, int)int(*)(int, int)传值推导,函数名转换为函数指针
    int add(int a, int b) { return a + b; } const auto fp = add;✅ 退化为函数指针int(*)(int, int)int(* const)(int, int)顶层 const 修饰函数指针本身
    int add(int a, int b) { return a + b; } auto& fp = add;❌ 不退化int(int, int)int(&)(int, int)引用绑定函数,保留函数类型
    int add(int a, int b) { return a + b; } auto* fp = add;✅ 退化为函数指针int(int, int)int(*)(int, int)根据 auto* 声明形式推导,最终得到函数指针
    int add(int a, int b) { return a + b; } auto&& fp = add;❌ 不退化int(&)(int, int)int(&)(int, int)函数名是左值,触发万能引用推导和引用折叠
    auto lam = [](int x) { return x * 2; };— 闭包类型闭包类型 Lambda 表达式推导为唯一的闭包类型,而非函数指针
推导案例
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <boost/type_index.hpp>
#include <iostream>

// 辅助打印推导类型(保留 const/volatile/引用 属性)
template <typename T>
void print_type(const char* name) {
std::cout << name << " = " << boost::typeindex::type_id_with_cvr<T>().pretty_name() << std::endl;
}

// 普通函数
int add(int a, int b) {
return a + b;
}

int main() {
// 案例 1:auto 推导函数名(传值方式),函数退化为函数指针
auto fp1 = add; // add 类型为 int(int, int),按值推导时函数退化为 int(*)(int, int),故 auto = int(*)(int, int),fp1 类型为 int(*)(int, int)
print_type<decltype(fp1)>("auto fp1 = add");

// 案例 2:const auto 推导函数名,const 修饰函数指针本身
const auto fp2 = add; // auto = int(*)(int, int),加上显式 const,fp2 类型为 int(* const)(int, int)
print_type<decltype(fp2)>("const auto fp2 = add");

// 案例 3:auto& 推导函数名(引用方式),保留函数类型
auto& fp3 = add; // auto& 绑定到函数本身,auto = int(int, int),fp3 类型为 int(&)(int, int)
print_type<decltype(fp3)>("auto& fp3 = add");

// 案例 4:auto* 推导函数名(指针方式)
auto* fp4 = add; // add 是函数左值,auto* 根据声明形式推导出 auto 为函数类型 int(int, int),最终 fp4 的类型为 int(*)(int, int),即指向函数的指针
print_type<decltype(fp4)>("auto* fp4 = add");

// 案例 5:auto&& 推导函数名(万能引用,左值)
auto&& fp5 = add; // add 是左值,auto&& 推 auto = int(&)(int, int),折叠后 fp5 类型为 int(&)(int, int)
print_type<decltype(fp5)>("auto&& fp5 = add");

// 案例 6:auto 推导无捕获的 lambda,得到闭包类型(非函数指针)
auto fp6 = [](int x) { return x * 2; }; // 闭包类型唯一且匿名,auto = 该闭包类型,fp6 类型为该闭包类型
print_type<decltype(fp6)>("auto fp6 = [](int x){ return x*2; }");

// 案例 7:auto 推导有捕获的 lambda,同样得到唯一的闭包类型
int y = 10;
auto fp7 = [y](int x) { return x + y; }; // 闭包类型唯一且匿名(且含捕获成员),auto = 该闭包类型,fp7 类型为该闭包类型
print_type<decltype(fp7)>("auto fp7 = [y](int x){ return x + y; }");

// 案例 8:auto& 推导有捕获的 lambda,保留闭包类型引用
auto& fp8 = fp7; // fp7 为闭包类型左值,auto& 绑定到该闭包对象本身,auto = 闭包类型,fp8 类型为该闭包类型的引用
print_type<decltype(fp8)>("auto& fp8 = fp7");

// 案例 9:无捕获的 lambda 可隐式转换为函数指针
int (*fp9)(int) = fp6; // fp6 为闭包类型(无捕获),可转换为 int(*)(int),fp9 类型为 int(*)(int)
print_type<decltype(fp9)>("int (*fp9)(int) = fp6");

// 案例 10:有捕获的 lambda 不能隐式转换为函数指针(编译错误)
// int (*fp10)(int) = fp7; // 编译失败:有捕获的 lambda 不能转为函数指针

return 0;
}

程序运行的结果如下:

1
2
3
4
5
6
7
8
9
auto fp1 = add = int (*)(int, int)
const auto fp2 = add = int (* const)(int, int)
auto& fp3 = add = int (&)(int, int)
auto* fp4 = add = int (*)(int, int)
auto&& fp5 = add = int (&)(int, int)
auto fp6 = [](int x){ return x*2; } = main::{lambda(int)#1}
auto fp7 = [y](int x){ return x + y; } = main::{lambda(int)#2}
auto& fp8 = fp7 = main::{lambda(int)#2}&
int (*fp9)(int) = fp6 = int (*)(int)
特殊类型推导
推导规则
  • std::initializer_list 类型推导规则的要点:

    • 花括号初始化列表与 auto 结合时,需要区分复制列表初始化和直接列表初始化。
    • 复制列表初始化:auto x = {1, 2, 3}; 中,auto 推导为 std::initializer_list<int>。
    • 直接列表初始化:auto x{1}; 中,C++ 11 / 14 会将 auto 推导为 std::initializer_list<int>;而 C++ 17 及之后 auto 推导为 int;如果包含多个元素,例如 auto x{1, 2};,则编译会失败。
    • 元素类型必须一致:auto x = {1, 2.5}; 无法推导出统一的元素类型,因此编译会失败。
    • 显式指定 std::initializer_list<T> 时,元素类型由 T 决定;列表中的元素必须能够转换为 T,且不能发生窄化转换。
    • std::initializer_list<T> 的元素不可通过该列表修改;即使列表对象本身不是 const,其元素也相当于 const T。
  • std::initializer_list 类型推导规则的总结:

    声明形式auto 的推导结果变量的最终类型说明
    auto a = {1, 2, 3};std::initializer_list<int>std::initializer_list<int>复制列表初始化,推导为 initializer_list
    auto a = {1};std::initializer_list<int>std::initializer_list<int>即使只有一个元素,也推导为 initializer_list
    auto a{1};intint直接列表初始化,单元素推导为元素类型
    auto a{1, 2};—— 编译失败,直接列表初始化不允许多个元素
    auto a = {1, 2.5};—— 编译失败,无法推导出统一的元素类型
    std::initializer_list<int> list = {1, 2, 3}; auto a = list;std::initializer_list<int>std::initializer_list<int>从已有对象推导,复制列表对象
    const auto a = {1, 2, 3};std::initializer_list<int>const std::initializer_list<int>const 由声明显式添加
    auto& a = list;std::initializer_list<int>std::initializer_list<int>&引用已有的列表对象
    auto&& a = list;std::initializer_list<int>&std::initializer_list<int>&list 是左值,发生引用折叠

关于 std::initializer_list 的介绍

std::initializer_list 是 C++ 11 引入的一个轻量级只读代理类,用于在花括号初始化列表中传递一组同类型元素,它本身不存储数据,只持有指向底层数组的首指针和长度,底层数组由编译器在栈上或静态存储区自动生成,生命周期与 initializer_list 对象绑定;它最常见的用途是让自定义容器或函数支持 {1, 2, 3} 这种列表初始化语法(如 std::vector<int> v = {1, 2, 3};),也常用于可变参数风格的函数接口(如 void f(std::initializer_list<int> args));使用时需注意三点:一是它的元素是 const 的,不能修改元素值;二是不要把它作为返回值或长期存储的容器,因为它指向的底层数组在离开作用域后即失效;三是它和 auto 类型推导配合使用时,auto x = {1, 2, 3}; 会得到 std::initializer_list<int>,而 auto x = {1, 2, 3.0}; 则会编译失败(元素类型不一致)。

推导案例
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <boost/type_index.hpp>
#include <initializer_list>
#include <iostream>

// 辅助打印推导类型(保留 const/volatile/引用 属性)
template <typename T>
void print_type(const char* name) {
std::cout << name << " = " << boost::typeindex::type_id_with_cvr<T>().pretty_name() << std::endl;
}

int main() {
// 案例 1:auto 推导花括号初始化列表,推导为 std::initializer_list<int>
auto a1 = {1, 2, 3}; // auto = std::initializer_list<int>,a1 类型为 std::initializer_list<int>
print_type<decltype(a1)>("auto a1 = {1, 2, 3}");

// 案例 2:auto 推导单元素花括号初始化列表,仍推导为 std::initializer_list<int>
auto a2 = {10}; // auto = std::initializer_list<int>,a2 类型为 std::initializer_list<int>
print_type<decltype(a2)>("auto a2 = {10}");

// 案例 3:auto 推导显式指定类型的 std::initializer_list 对象
std::initializer_list<int> list = {1, 2, 3};
auto a3 = list; // auto = std::initializer_list<int>,a3 类型为 std::initializer_list<int>
print_type<decltype(a3)>("auto a3 = list");

// 案例 4:const auto 推导花括号初始化列表,列表对象本身具有 const 属性
const auto a4 = {1, 2, 3}; // auto = std::initializer_list<int>,a4 类型为 const std::initializer_list<int>
print_type<decltype(a4)>("const auto a4 = {1, 2, 3}");

// 案例 5:auto& 引用花括号初始化列表对象
auto& a5 = a1; // auto = std::initializer_list<int>,a5 类型为 std::initializer_list<int>&
print_type<decltype(a5)>("auto& a5 = a1");

// 案例 6:auto&& 万能引用绑定花括号初始化列表对象的左值
auto&& a6 = a1; // a1 是左值,auto 推导为 std::initializer_list<int>&,经引用折叠后 a6 类型为 std::initializer_list<int>&
print_type<decltype(a6)>("auto&& a6 = a1");

// 案例 7:auto 推导元素类型不一致的花括号初始化列表(编译错误)
// auto a7 = {1, 2.5}; // 编译失败:std::initializer_list 的元素类型无法统一推导

// 案例 8:auto 直接列表初始化单个元素
auto a8{1}; // C++ 11/14 中,auto 推导为 std::initializer_list<int>,C++ 17 及之后,auto 推导为 int
print_type<decltype(a8)>("auto a8{1}");

return 0;
}

程序运行的结果如下:

1
2
3
4
5
6
7
auto a1 = {1, 2, 3} = std::initializer_list<int>
auto a2 = {10} = std::initializer_list<int>
auto a3 = list = std::initializer_list<int>
const auto a4 = {1, 2, 3} = std::initializer_list<int> const
auto& a5 = a1 = std::initializer_list<int>&
auto&& a6 = a1 = std::initializer_list<int>&
auto a8{1} = int

特别注意

  • 这里的 auto a = {1, 2, 3}; 会将 auto 推导为 std::initializer_list<int>;但 auto a{1}; 使用直接列表初始化,在 C++ 17 及之后会将 auto 推导为 int,两者不要混淆。

与模板类型推导的区别

auto 类型推导与模板类型推导的区别如下:

  • 相似点:

    • auto 和模板中的 T 都需要编译器在编译期根据初始化的表达式来推导出具体的类型。
    • 两者都遵循类似的 “退化” 规则(例如:都会忽略顶层 const,数组会退化为指针,函数会退化为函数指针)。
    • 两者在引用推导上大多相似,auto&& 和模板中的 T&& 都属于万能引用,这部分行为完全一致。在 const 修饰的引用推导细节上,两者并没有本质的推导规则差别,规则完全相同。
  • 不同点:

    • 语法角色不同:
      • T 是模板参数,需要显式实例化(如 func<int>(x))或通过实参推导。
      • auto 是 C++ 11 引入的类型占位符,主要用于变量声明,让编译器自动填充类型。
    • 类型推导规则存在例外(最重要的一点):
      • 对于列表初始化 {},auto 会将其推导为 std::initializer_list<T>(例如 auto x = {1, 2, 3}; 会被推导为 std::initializer_list<int>)。
      • 而模板类型推导无法直接推导初始化列表 {}(除非模板参数本身就是 std::initializer_list<T>)。

总结

auto 可以简单类比为模板类型推导中的类型参数(T),但它本质上是一个类型占位符,且其推导规则与模板类型推导存在关键差异(例如对初始化列表 {} 的处理)。