C++ 从入门到精通之十九
大纲
- C++ 从入门到精通之一、C++ 从入门到精通之二、C++ 从入门到精通之三
- C++ 从入门到精通之四、C++ 从入门到精通之五、C++ 从入门到精通之六
- C++ 从入门到精通之七、C++ 从入门到精通之八、C++ 从入门到精通之九
- C++ 从入门到精通之十、C++ 从入门到精通之十一、C++ 从入门到精通之十二
- C++ 从入门到精通之十三、C++ 从入门到精通之十四、C++ 从入门到精通之十五
- C++ 从入门到精通之十六、C++ 从入门到精通之十七、C++ 从入门到精通之十八
- C++ 从入门到精通之十九、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&&,不发生引用折叠
- (1) 根据初始化表达式的值类别推导
- 举例:
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 = #— ❌ 不丢弃 const intconst int*指针推导,保留被指向对象的 const属性const int num = 10; const auto* x = #— ❌ 不丢弃 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 |
|
程序运行的结果如下:
1 | auto a1 = x = 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 |
|
程序运行的结果如下:
1 | auto str1 = 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 |
|
程序运行的结果如下:
1 | auto fp1 = add = int (*)(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_listauto a = {1};std::initializer_list<int>std::initializer_list<int>即使只有一个元素,也推导为 initializer_listauto 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 |
|
程序运行的结果如下:
1 | auto a1 = {1, 2, 3} = std::initializer_list<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保留顶层 constvolatile int v;decltype(v)volatile int保留顶层 volatileconst volatile int cv = 0;decltype(cv)const volatile int同时保留 const和volatileint& 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 |
|
程序运行的结果如下:
1 | decltype(c) = int |
作用于表达式
推导规则
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 |
|
程序运行的结果如下:
1 | decltype(a) = 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 |
|
程序运行的结果如下:
1 | decltype(a) = int () |
使用案例
使用案例一
decltype应付模板编程中可变类型
代码写法一,不使用
decltype,编译成功
1 |
|
代码写法二,不使用
decltype,编译失败
1 |
|
代码写法三,使用
decltype,解决编译失败的问题
1 |
|
使用案例二
decltype通过变量表达式抽取变量类型
1 |
|
程序运行的结果如下:
1 | size = 3 |
使用案例三
C++ 版本说明
- C++ 11:支持
auto类型推导和decltype类型推导,但不能将二者组合成decltype(auto)来使用。 - C++ 14 及以后:支持
decltype(auto),可以根据初始化表达式或返回表达式,按照decltype的规则推导类型,并保留引用、const等类型属性。
decltype(auto)用于变量声明
1 |
|
decltype(auto)用于函数返回类型
1 |
|
使用案例四
什么是函数返回类型尾置语法
函数返回类型尾置语法(Trailing Return Type)是 C++ 11 引入的一种函数声明语法,使用 auto 声明函数,并通过 -> 将返回类型放在参数列表之后。该语法通常与 decltype 配合使用,可以根据参数表达式推导函数的返回类型,尤其适用于模板编程中返回类型依赖于参数类型的场景。使用案例如下:
1 | auto add(int i, int k) -> decltype(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 |
|
程序运行的结果如下:
1 | int &tf(int &i) |
代码分析
- 演示了函数返回类型尾置语法结合
decltype,根据模板参数及函数重载结果推导函数返回类型的用法。 Func()使用函数返回类型尾置语法,通过decltype(tf(t))根据tf(t)表达式推导返回类型。- 当传入
int类型的左值时,匹配int& tf(int& i),因此Func()的返回类型为int&。 - 当传入
double类型的左值时,匹配double tf(double& d),因此Func()的返回类型为double。 - 注意,这里的
auto并没有类型推导的语义,它只是函数返回类型尾置语法的组成部分。
