C++ auto与decltype及函数返回类型后置
前言:在早期C/C++中auto关键字的作用是:一个存储类型指示符,使用auto修饰的变量,存储类型为自动存储期,从变量声明处生命周期开始,出变量所在代码块生命周期结束,并且全局变量不能用auto修饰。但是局部变量的生命周期本来就是进入作用域生命周期开始,出作用域生命周期结束。导致用auto修饰局部变量和不使用auto修饰没有任何区别,处于一个尴尬地步。
一、auto
1.1 C++11
C++11中,标准委员会赋予了auto全新的作用:auto做为类型占位符,auto声明的变量数据类型由编译器在编译时推导而得,所以使用auto声明变量时必须对其进行初始化,在编译阶段编译器根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译阶段会将auto替换为变量实际的类型。
1 |
|
输出:
1 | y:int |
注意:使用auto对多个变量推导时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器按第一个变量类型进行推导,然后用推导出来的类型定义其他变量
1 | auto a = 10, b = 20; // 正确 |
1.1.1 推导规则
规则1: auto声明变量时没有使用引用或指针,则推导出来类型会忽略引用、const 限定符和 volatile 限定符
1 | const int i = 2025; |
规则2: auto推导指针类型时,auto与auto*没有任何区别。
1 | const int i = 1; |
规则3: auto推导目标对象是数组或函数时,会被推导为对应指针类型
1 | void fun() { |
规则4: auto推导列表表达式
注意:下面规则适用于C++ 17
- 直接使用列表初始化,列表中必须为单元素,否则编译错误,auto推导类型为该元素类型
- 用等号加列表初始化,列表中可以包含多个类型相同的元素,auto推导类型为
std::initializer_list<T>,其中T为元素类型
1 | auto a1{3}; // auto推导类型为int |
规则5: auto结合万能引用推导
如果推导对象是一个左值,将被推导为引用类型。如果推导对象是一个右值,将被推导为右值引用类型
1 | int a = 2025; |
1.1.2 auto不能使用的场景
1.1.2.1 auto 不能做为函数形参
1 | int add(auto x, auto y) // 错误,auto不能做函数形参,编译器不知道如何为形参分配空间 |
1.1.2.2 auto 不能声明数组
1 | auto arr[] = {1, 2, 3}; // 错误 |
1.2 C++ 14
1.2.1 函数返回值类型推导
C++ 14支持使用auto对函数返回值类型进行推导,但要确保所有的返回值类型是相同的。
1 | auto add(int i,int j) // 正确 |
1.2.2 lambda
C++14支持在lambda中使用auto作为形参以及返回值类型推导。
1 | auto f = [](auto x, auto y) // lambda中使用auto对函数形参类型推导 |
1.3 C++ 17
1.3.1 非类型模版参数
C++ 17支持auto作为非类型模版参数的占位符,但推导出来的类型必须是符合非类型模版参数类型要求的,否则编译会错误。
1 | template <auto N> |
说明:对于类型复杂的容器、迭代器、lambda等,auto 可以简化代码,使其更加简洁易读。如果滥用auto,可能会导致类型不明确,使代码失去可读性。
二、decltype
2.1 C++11
在C++11以前,C++标准提供typeid运算符来查询变量的类型,这种类型查询在运行时进行。RTTI机制为每一个类型产生一个type_info的对象,typeid查询变量对应type_info。RTTI会导致运行时效率降低,且在泛型编程中,我们更需要编译时就确定类型,RTTI无法满足这样的要求。编译时类型推导的出现正是为了泛型编程,在非泛型编程中,我们的类型都是确定的,根本不需要再进行推导。C++11提供decltype关键字,它在编译时推导表达式的类型,而无需计算该表达式。这对于泛型编程、模板编程以及复杂类型的推导特别有用。
语法:decltype( expression )
作用:返回expression参数类型
2.1.1 推导规则
- 如果 expression 参数是未加括号到标识符或类成员访问,则 decltype(expression) 推导是
T。如果不存在此类实体或 expression 参数命名一组重载函数,则编译器将生成错误消息。 - 如果 expression 参数是对一个函数或一个重载运算符函数的调用,则 decltype(expression) 推导是函数的返回类型
- 如果 expression 参数是将亡值,则 decltype(expression) 推导是
T&&类型。 如果 expression 参数是左值,则 decltype(expression) 推导是T& - 除去上面情况,则 decltype(expression) 推导是
T
1 | int var; |
2.1.2 CV限定符推导
CV 限定符(CV-qualifiers)指的是 const 和 volatile 关键字。它们用于限定变量、对象、类型,使得编译器对这些对象的处理方式有所不同。
1 | /* |
2.1.3 示例
在日常中我们经常会用求和函数,如果指定了函数形参类型则不够通用,下面使用模版实现一个通用求和函数。
1 | // C++11并不支持auto占位的函数返回类型进行推导,需要结合后置返回类型上的decltype说明符 |
上面这个示例又会让人产生疑问,C++14中decltype的作用似乎又被auto取代了,但并不是,使用auto占位的函数返回类型推导时,如果期望返回类型是引用,但auto占位只能返回值类型。
1 | // 下列代码在C++14 中测试 |
2.2 C++ 14
在C++14中,支持使用decltype(auto) 来进行类型推导,它实质是将表达式代入到auto然后再用decltype规则进行推导。注意decltype(auto)必须单独声明,不能结合指针、引用以及CV限定符。
1 | int i; |
在2.1.3示例中如果使用auto占位函数返回类型进行推导,如果期望返回值是引用需要结合后置返回类型上的decltype说明符,有了decltype(auto)后代码可简写为
1 | template <class T> |
2.3 C++ 17
C++ 17支持decltype(auto)作为非类型模版参数的占位符,但推导出来的类型必须是符合非类型模版参数类型要求的,否则编译会错误。
1 | template <decltype(auto) N> |
三、函数返回类型后置(C++ 11)
C++ 11支持函数返回类型后置,使用auto在返回类型位置进行占位,表示“返回类型将会稍后引出或指定”,->后才是真正返回类型。
语法:
1 | auto fun(arg...) -> ret_type |
3.1 配合函数模版
在C++ 11以前,如果有下面这个函数模版,那么函数返回类型应该写什么呢?
1 | template<class T1, class T2> |
可能一开始就想到使用decltype去推导x+y的类型,写出下面代码,但是会编译错误。因为编译器在解析返回类型时还没有解析到参数部分,对x和y类型一无所知。
1 | template<class T1, class T2> |
下面代码则可以编译通过,先将nullptr转换为T1和T2类型指针,然后解引用求和,decltype类型推导时不会真正计算表达式,所以这里求和不会有问题。不过这种写法不易懂且代码不美观。
1 | template <class T1, class T2> |
在C++11中我们可以使用下面代码解决上面问题。
1 | template<class T1, class T2> |
3.2 返回复杂类型
当要返回复杂类型,例如返回函数指针类型时,使用函数返回类型后置写法比较简洁。
1 | int f1(int x) |
参考
本文参考了谢丙堃 <<现代C++语言核心特性解析>>,在此先表示感谢,如涉及版权问题,版权所有为原作者。