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

大纲

C++ 进阶

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}; 则会编译失败(元素类型不一致)。更多关于 std::initializer_list 的详细介绍可以看 这里。

推导案例
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),但它本质上是一个类型占位符,且其推导规则与模板类型推导存在关键差异(例如对初始化列表 {} 的处理)。

decltype 类型推导说明符

核心概念

decltype 是 C++ 11 引入的类型推导说明符,用于在编译期获取实体或表达式的类型,而不对表达式进行求值。它与 auto 的主要区别在于类型推导规则不同:auto 通常按照类似函数模板实参推导的规则确定类型,而 decltype 对未加括号的变量名等特殊表达式采用实体的声明类型,对其他表达式则根据表达式的值类别确定类型,因此能够准确保留引用以及 const、volatile 等类型限定符。decltype 常用于泛型编程、尾置返回类型、类型萃取以及 SFINAE 等场景,是编写通用模板代码的重要工具。

适用场景

  • decltype 的适用场景

    • 泛型编程中推导表达式类型:当表达式的类型依赖模板参数、难以手动指定时,可以使用 decltype 获取表达式的类型。
    • 尾置返回类型:当函数的返回类型依赖参数表达式时,可以使用 decltype 推导返回类型。
    • 完美转发:配合万能引用(转发引用)和 std::forward,根据模板参数的推导结果保留实参的值类别(左值 / 右值)。
    • 保留 cv 限定符与引用:需要保留 const、volatile 及引用类型时,可以使用 decltype 精确获取实体或表达式的类型。
    • 声明与某实体或表达式具有相同类型的变量:例如 decltype(x) y = ...;,使 y 的类型按照 decltype(x) 的规则确定。
    • 萃取函数返回类型:使用 decltype(f()) 获取函数调用表达式的类型,而不会实际调用函数。
    • 类型萃取与模板元编程:使用 decltype 获取表达式的类型,并将其用于类型别名、模板参数及其他类型计算。
    • SFINAE 与类型检测:结合模板替换规则,利用 decltype 检测表达式是否合法,例如检测某个类型是否支持指定的成员操作。
  • decltype 的不适用场景

    • 需要获取表达式的运行结果:decltype 只用于编译期类型分析,不会对表达式进行求值,因此不能获取表达式的实际运行结果。
    • 希望采用 auto 的常规类型推导规则:如果希望通常忽略顶层 const 和引用,应考虑使用 auto;但 auto 的具体推导行为仍取决于声明形式。
    • 直接获取花括号初始化列表的类型:decltype({1, 2, 3}) 不合法,因为花括号初始化列表不是表达式;应根据需求使用 auto 或显式指定类型。
    • 直接获取函数指针类型:对于函数名 f,decltype(f) 得到函数类型而不是函数指针类型;需要函数指针时,可以使用 decltype(f)*。
    • 希望自动进行数组到指针退化:decltype(arr) 会保留数组类型,例如 int[3],不会自动退化为 int*;需要时应显式进行相应的类型转换或声明。
    • 进行运行时类型判断:decltype 用于编译期类型推导,不能获取运行时才确定的对象类型;需要运行时类型识别时,可根据场景考虑 dynamic_cast 或 typeid。
    • 使用未声明或不可访问的实体:相关名字必须满足名称查找、作用域及访问控制等规则,否则可能导致编译失败。
    • 直接替代类型别名声明:decltype 本身不能替代 using 或 typedef 的命名作用,但可以配合它们定义类型别名。

推导规则

特别注意

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

作用于变量名
推导规则
  • decltype 作用于变量名的推导规则:

    • 对未加括号的变量名使用 decltype 是一种特殊情况,直接返回该变量的声明类型,完整保留 const、volatile 及引用类型等信息。
    • 与 auto 的关键区别:
      • const int a = 10; auto x = a; 中,auto 按常规推导规则忽略顶层 const,推导结果为 int。
      • const int a = 10; decltype(a) d = 0; 中,decltype(a) 保留变量 a 的声明类型,推导结果为 const int。
    • 变量名加上括号的区别:
      • decltype(x) → int(未加括号的变量名,直接返回声明类型)。
      • decltype((x)) → int&(加括号后的变量名属于普通表达式,x 是左值,因此推导为左值引用)。
      • 注意:有名字的右值引用变量本身是左值表达式,因此 decltype((rr)) 的结果是左值引用类型 int&,而不是右值引用类型 int&&。
  • decltype 作用于变量名的常见案例:

    变量声明decltype 使用示例推导结果说明
    int x;decltype(x)int未加括号的变量名,返回声明类型
    const int a = 0;decltype(a)const int保留顶层 const
    volatile int v;decltype(v)volatile int保留顶层 volatile
    const volatile int cv = 0;decltype(cv)const volatile int同时保留 const 和 volatile
    int& r = x;decltype(r)int&保留左值引用类型
    const int& b = a;decltype(b)const int&保留引用和 const 限定符
    int&& rr = 0;decltype(rr)int&&保留右值引用类型
    int arr[3];decltype(arr)int[3]保留数组类型,不退化为指针
    int* p;decltype(p)int*返回指针类型
    MyClass obj;decltype(obj)MyClass返回类对象的声明类型
    int x;decltype((x))int&加括号后,x 是左值表达式,推导为左值引用
    const int a = 0;decltype((a))const int&加括号后,保留 const 并推导为左值引用
    int&& rr = 0;decltype((rr))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
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
70
#include <boost/type_index.hpp>
#include <iostream>

// 自定义类
class MyClass {
public:
float i;
double j;
};

// 辅助打印推导类型(保留 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 = 0;
int y = 0;
const int a = 1;
const int& b = a;
MyClass my_class;

// 案例 1:auto 值传递,顶层 const 和引用均被丢弃
auto c = a; // auto = int,c 类型为 int
print_type<decltype(c)>("decltype(c)");

// 案例 2:decltype 作用于变量名
decltype(a) d = 15; // decltype(a) = const int,d 类型为 const int
print_type<decltype(d)>("decltype(d)");

// 案例 3:decltype 作用于引用变量名
decltype(b) e = d; // decltype(b) = const int&,e 类型为 const int&
print_type<decltype(e)>("decltype(e)");

// 案例 4:decltype 作用于类类型对象
decltype(my_class) f; // decltype(my_class) = MyClass,f 类型为 MyClass
print_type<decltype(f)>("decltype(f)");

// 案例 5:decltype 作用于类数据成员
decltype(MyClass::i) g = 5.0; // decltype(MyClass::i) = float,g 类型为 float
print_type<decltype(g)>("decltype(g)");

// 案例 6:auto&& 作为万能引用绑定左值
auto&& h = x; // auto = int&,int& && 经引用折叠变为 int&,h 类型为 int&
print_type<decltype(h)>("decltype(h)");

// 案例 7
decltype(h)&& i = y; // decltype(h) = int&,int& && 经引用折叠变为 int&,i 类型为 int&
print_type<decltype(i)>("decltype(i)");

// 案例 8:auto 推导数组——数组退化为指针
int arr[3] = {1, 2, 3};
auto k = arr; // arr 退化为指向首元素的指针,auto = int*,k 类型为 int*
print_type<decltype(k)>("decltype(k)");

// 案例 9:decltype 作用于数组名——保留完整数组类型,不退化为指针
decltype(arr) l; // decltype(arr) = int[3],l 类型为 int[3]
print_type<decltype(l)>("decltype(l)");

// 案例 10:decltype 作用于数组下标表达式——返回左值,得到元素引用类型
decltype(arr[0]) m = x; // decltype(arr[0]) = int&,m 类型为 int&
print_type<decltype(m)>("decltype(m)");

// 案例 11:decltype 作用于数组名加上括号——退化为左值表达式,得到数组引用类型
decltype((arr)) n = arr; // decltype((arr)) = int(&)[3],n 类型为 int(&)[3]
print_type<decltype(n)>("decltype(n)");

return 0;
}

程序运行的结果如下:

1
2
3
4
5
6
7
8
9
10
11
decltype(c) = int
decltype(d) = int const
decltype(e) = int const&
decltype(f) = MyClass
decltype(g) = float
decltype(h) = int&
decltype(i) = int&
decltype(k) = int*
decltype(l) = int [3]
decltype(m) = int&
decltype(n) = int (&) [3]
作用于表达式
推导规则
  • decltype 作用于表达式的推导规则:

    • 对于不属于未加括号的变量名等特殊情况的表达式,decltype 根据表达式的值类别进行推导:
      • (1) 若表达式是纯右值(prvalue),返回表达式的类型本身,不带引用。
      • (2) 若表达式是左值(lvalue),返回 T&。
      • (3) 若表达式是将亡值(xvalue),返回 T&&。
    • decltype 的注意事项:
      • 对未加括号的变量名使用 decltype 是一种特殊情况,直接返回变量的声明类型;而使用 decltype((x)) 时,括号内的 x 是左值表达式,因此推导结果为 int&。这就是 decltype(x) 与 decltype((x)) 结果不同的原因。
      • decltype 不会对表达式进行求值,因此不会执行表达式中的运行时操作。例如,decltype(x++) 不会实际执行自增,decltype(x = 5) 不会实际执行赋值,decltype(*p) 也不会实际执行指针解引用。但这些表达式仍然必须满足相应的语法和语义要求,才能通过编译。
  • decltype 作用于表达式的常见案例:

    • 注意,在下面的表格内容中:x、y 和 p 假设分别为 int 类型的变量、int 类型的变量和 int* 类型的指针;f() 假设返回非引用类型。
    • 涉及 ++、--、赋值等操作时,以下结论针对内置运算符,重载运算符的行为可能不同。
      表达式类别decltype 使用示例推导结果说明
      左值decltype((x))int&x 是左值表达式,因此推导为左值引用
      左值decltype(x = 5)int&内置赋值表达式的结果是左值
      左值decltype(++x)int&内置前置自增表达式的结果是左值
      左值decltype(*p)int&当 p 是 int* 时,内置指针解引用表达式的结果是左值
      左值decltype(true ? x : x)int&当 x 是 int 左值时,条件表达式的结果也是左值
      左值decltype((void)x, x)int&逗号表达式的结果具有最后一个操作数的值类别和类型;这里最后一个操作数 x 是左值
      纯右值decltype(x++)int内置后置自增表达式的结果是纯右值
      纯右值decltype(x + y)int当 x、y 为 int 时,内置加法表达式的结果是纯右值
      纯右值decltype(f())函数返回类型当 f() 的返回类型为非引用类型时,结果为该返回类型
      将亡值decltype(std::move(x))int&&当 x 是 int 类型的左值时,std::move(x) 是类型为 int&& 的将亡值表达式
      将亡值decltype(static_cast<int&&>(x))int&&将 x 转换为 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
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 main() {
int x = 0;
int& r = x;
const int c = 1;

// 案例 1:decltype 作用于字面量(纯右值)——返回其类型本身,非引用
decltype(5) a = 0; // decltype(5) = int,a 类型为 int
print_type<decltype(a)>("decltype(a)");

// 案例 2:decltype 作用于左值表达式(带括号的变量),得到引用类型
decltype((x)) b = x; // 变量名 x 单独用时是 int,但 (x) 是左值表达式,decltype((x)) 返回 int&,b 类型为 int&
print_type<decltype(b)>("decltype(b)");

// 案例 3:decltype 作用于赋值表达式——赋值返回左值,得到引用类型
decltype(x = 5) d = x; // decltype(x = 5) = int&,d 类型为 int&
print_type<decltype(d)>("decltype(d)");

// 案例 4:decltype 作用于前置自增表达式——返回左值,得到引用类型
decltype(++x) e = x; // decltype(++x) = int&,e 类型为 int&
print_type<decltype(e)>("decltype(e)");

// 案例 5:decltype 作用于后置自增表达式——返回纯右值,得到非引用类型
decltype(x++) f = 0; // decltype(x++) = int,f 类型为 int
print_type<decltype(f)>("decltype(f)");

// 案例 6:decltype 作用于算术表达式——返回纯右值,得到非引用类型
decltype(x + c) g = 0; // decltype(x + c) = int,g 类型为 int
print_type<decltype(g)>("decltype(g)");

// 案例 7:decltype 作用于返回右值引用的函数调用(结果为将亡值)
decltype(std::move(x)) h = 0; // std::move 返回 int&&,h 类型为 int&&
print_type<decltype(h)>("decltype(h)");

// 案例 8:decltype 作用于解引用表达式——返回左值,得到引用类型
int* p = &x;
decltype(*p) i = x; // decltype(*p) = int&,i 类型为 int&
print_type<decltype(i)>("decltype(i)");

// 案例 9:decltype 作用于三元表达式——两个分支均为左值时返回左值
decltype(true ? x : x) j = x; // decltype(true ? x : x) = int&,j 类型为 int&
print_type<decltype(j)>("decltype(j)");

// 案例 10:decltype 作用于逗号表达式——结果为最后一个表达式的类型和值类别
decltype((void)x, x) k = x; // 逗号表达式结果为左值 x,k 类型为 int&
print_type<decltype(k)>("decltype(k)");

return 0;
}

程序运行的结果如下:

1
2
3
4
5
6
7
8
9
10
decltype(a) = int
decltype(b) = int&
decltype(d) = int&
decltype(e) = int&
decltype(f) = int
decltype(g) = int
decltype(h) = int&&
decltype(i) = int&
decltype(j) = int&
decltype(k) = int&
作用域函数名
推导规则
  • decltype 作用于函数名的推导规则:

    • 对未加括号的函数名使用 decltype,返回函数的声明类型本身,而非函数指针,不会发生函数到函数指针的转换。
    • 要点:
      • decltype(f) 与 decltype(&f) 不同:前者得到函数类型 int(),后者得到函数指针类型 int(*)()。
      • 这与 auto 的推导规则不同:auto x = f; 中,函数名 f 会发生函数到函数指针的转换,推导结果为 int(*)()。
      • 函数名作为表达式时属于左值,但 decltype 对未加括号的函数名采用特殊规则,直接返回函数的声明类型,不附加引用。
      • 对函数名加上括号后,decltype((f)) 按普通表达式规则处理。由于 (f) 是左值表达式,因此推导结果为函数左值引用类型 int(&)()。
      • 特别注意,decltype 只进行编译期类型分析,不会实际调用函数。
  • decltype 作用于函数名的常见案例:

    函数声明decltype 使用示例推导结果说明
    int f();decltype(f)int()返回函数类型本身
    int& f();decltype(f)int&()返回类型为 int& 的函数类型
    void f(int);decltype(f)void(int)函数类型包含返回类型和参数类型
    int f();decltype(&f)int(*)()取函数地址,得到函数指针类型
    int f();decltype(f)*int(*)()在函数类型后添加指针,得到函数指针类型
    int f();decltype(f)&int(&)()在函数类型后添加左值引用,得到函数左值引用类型
    int f();decltype((f))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
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
#include <boost/type_index.hpp>
#include <functional>
#include <iostream>

int f() {
return 0;
}

int& g() {
static int v = 0;
return v;
}

void h(int) {
}

// 辅助打印推导类型(保留 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:decltype 作用于返回 int 的函数名——返回函数类型本身
decltype(f) a; // decltype(f) = int(),a 类型为 int()
print_type<decltype(a)>("decltype(a)");

// 案例 2:decltype 作用于返回 int& 的函数名——保留返回引用
decltype(g) b; // decltype(g) = int&(),b 类型为 int&()
print_type<decltype(b)>("decltype(b)");

// 案例 3:decltype 作用于带参数的函数名——参数列表也是函数类型的一部分
decltype(h) c; // decltype(h) = void(int),c 类型为 void(int)
print_type<decltype(c)>("decltype(c)");

// 案例 4:decltype 作用于函数名后显式加 *——得到函数指针类型
decltype(f)* d = f; // decltype(f)* = int(*)(), d 类型为 int(*)()
print_type<decltype(d)>("decltype(d)");

// 案例 5:decltype 作用于函数名后显式加 &——得到函数引用类型
decltype(f)& e = f; // decltype(f)& = int(&)(), e 类型为 int(&)()
print_type<decltype(e)>("decltype(e)");

// 案例 6:decltype 作用于加括号的函数名——退化为左值表达式,返回函数引用
decltype((f)) i = f; // decltype((f)) = int(&)(), i 类型为 int(&)()
print_type<decltype(i)>("decltype(i)");

// 案例 7:decltype 结合 std::function 使用
std::function<decltype(f)> j = f; // decltype(f) = int(),作为模板参数传入 std::function,得到 std::function<int()>,j 类型为 std::function<int()>,可用于包装函数 f
print_type<decltype(j)>("decltype(j)");

return 0;
}

程序运行的结果如下:

1
2
3
4
5
6
7
decltype(a) = int ()
decltype(b) = int& ()
decltype(c) = void (int)
decltype(d) = int (*)()
decltype(e) = int (&)()
decltype(i) = int (&)()
decltype(j) = std::function<int ()>

使用案例

使用案例一
  • decltype 应付模板编程中可变类型

代码写法一,不使用 decltype,编译成功

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>
#include <vector>

template <typename T>
class MyClass {
public:
// 定义一个类型为 T::iterator 的迭代器变量 iter
typename T::iterator iter;

void get_begin(T& tmp) {
iter = tmp.begin();
}
};

int main() {
// 定义类型别名 container_type,表示元素类型为 int 的 std::vector 容器类型
using container_type = std::vector<int>;

container_type arr = {1, 2, 3, 4, 5};
MyClass<container_type> mc;
mc.get_begin(arr);
return 0;
}

代码写法二,不使用 decltype,编译失败

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>
#include <vector>

template <typename T>
class MyClass {
public:
// 定义一个类型为 T::iterator 的迭代器变量 iter,iterator 是非常量迭代器,不能直接用于遍历常量容器
typename T::iterator iter;

void get_begin(T& tmp) {
iter = tmp.begin(); // 当 T 为常量容器类型时,tmp.begin() 返回 const_iterator,与 iter 的 iterator 类型不匹配,导致编译失败
}
};

int main() {
// 定义类型别名 container_type,表示元素类型为 int 的 const std::vector 容器类型(常量容器类型)
using container_type = const std::vector<int>;

container_type arr = {1, 2, 3, 4, 5};
MyClass<container_type> mc;
mc.get_begin(arr);
return 0;
}

代码写法三,使用 decltype,解决编译失败的问题

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 <iostream>
#include <vector>

template <typename T>
class MyClass {
public:
// 定义迭代器变量 iter,使用 decltype 推导 T 的 begin() 函数返回值类型,并将其作为迭代器的类型
// decltype(T().begin()) 仅用于编译期类型推导,不会实际创建 T 的临时对象,也不会调用 begin() 函数
// 这里的 T().begin() 是用于描述表达式类型的语法形式,而不是实际执行的函数调用
decltype(T().begin()) iter;

void get_begin(T& tmp) {
iter = tmp.begin();
}
};

int main() {
// 定义类型别名 container_type,表示元素类型为 int 的 const std::vector 容器类型(常量容器类型)
using container_type = const std::vector<int>;

container_type arr = {1, 2, 3, 4, 5};
MyClass<container_type> mc;
mc.get_begin(arr);
return 0;
}
使用案例二
  • decltype 通过变量表达式抽取变量类型
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 <iostream>
#include <vector>

int main() {
std::vector<int> vec;
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);

std::vector<int>::size_type size = vec.size();
std::cout << "size = " << size << std::endl;

// 案例 1
// 使用 decltype(vec) 获取变量 vec 的类型 std::vector<int>,再通过 ::size_type 获取其成员类型
// 等价于 std::vector<int>::size_type size2 = size;
decltype(vec)::size_type size2 = size;
std::cout << "size2 = " << size2 << std::endl;

// 案例 2
typedef decltype(sizeof(0)) m_size_t; // 使用 decltype 推导 sizeof(0) 的结果类型,并将其定义为 m_size_t 类型别名
typedef decltype(sizeof(int)) m_size_t; // 使用 decltype 推导 sizeof(int) 的结果类型,并将其定义为 m_size_t 类型别名,与上一行等价
m_size_t num;

return 0;
}

程序运行的结果如下:

1
2
size = 3
size2 = 3
使用案例三

C++ 版本说明

  • C++ 11:支持 auto 类型推导和 decltype 类型推导,但不能将二者组合成 decltype(auto) 来使用。
  • C++ 14 及以后:支持 decltype(auto),可以根据初始化表达式或返回表达式,按照 decltype 的规则推导类型,并保留引用、const 等类型属性。
  • decltype(auto) 用于变量声明
1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

int main() {
int x = 1;
const int& y = x;

auto z = y; // auto 推导为 int,会忽略引用属性并丢弃顶层 const
decltype(auto) z2 = y; // decltype(auto) 遵循 decltype 的推导规则,推导为 const int&,保留 const 和引用属性

return 0;
}

  • decltype(auto) 用于函数返回类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

template <typename T>
decltype(auto) process(T& v) { // 使用 decltype(auto) 根据返回表达式推导返回类型,保留引用等类型属性
v *= 2;
return v; // v 是具名变量表达式,属于左值,因此返回类型为 T&
}

int main() {
int a = 100;
process(a) = 20; // process(a) 返回 int& 时,可以直接修改变量 a;若将上面的 decltype(auto) 改为 auto,则返回类型为 int,函数调用结果是纯右值,不能作为赋值运算符的左操作数,会导致编译失败
std::cout << a << std::endl; // 输出 20

return 0;
}
使用案例四

什么是函数返回类型尾置语法

函数返回类型尾置语法(Trailing Return Type)是 C++ 11 引入的一种函数声明语法,使用 auto 声明函数,并通过 -> 将返回类型放在参数列表之后。该语法通常与 decltype 配合使用,可以根据参数表达式推导函数的返回类型,尤其适用于模板编程中返回类型依赖于参数类型的场景。使用案例如下:

1
2
3
auto add(int i, int k) -> decltype(i + k) {
return i + k;
}

代码分析

上述代码使用 auto 声明函数 add,通过 -> decltype(i + k) 将返回类型声明在参数列表之后。decltype(i + k) 用于推导表达式 i + k 的类型,由于 i 和 k 均为 int 类型,因此函数的返回类型为 int。上述代码等价于传统的 int add(int i, int k) 写法。

auto 结合 decltype 构成函数返回类型尾置语法

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 <iostream>

// 重载函数一
int &tf(int &i) {
std::cout << "int &tf(int &i)" << std::endl;
return i;
}

// 重载函数二
double tf(double &d) {
std::cout << "double tf(double &d)" << std::endl;
return d;
}

template <typename T>
auto Func(T &t) -> decltype(tf(t)) { // 使用函数返回类型尾置语法,根据 tf(t) 的表达式类型推导 Func 的返回类型
return tf(t); // 调用与参数类型匹配的 tf() 重载函数,并返回结果
}

int main() {
int i = 10;
Func(i); // T 推导为 int,decltype(tf(t)) 推导为 int&

double j = 20.5f;
Func(j); // T 推导为 double,decltype(tf(t)) 推导为 double

return 0;
}

程序运行的结果如下:

1
2
int &tf(int &i)
double tf(double &d)

代码分析

  • 演示了函数返回类型尾置语法结合 decltype,根据模板参数及函数重载结果推导函数返回类型的用法。
  • Func() 使用函数返回类型尾置语法,通过 decltype(tf(t)) 根据 tf(t) 表达式推导返回类型。
  • 当传入 int 类型的左值时,匹配 int& tf(int& i),因此 Func() 的返回类型为 int&。
  • 当传入 double 类型的左值时,匹配 double tf(double& d),因此 Func() 的返回类型为 double。
  • 注意,这里的 auto 并没有类型推导的语义,它只是函数返回类型尾置语法的组成部分。