C++ auto与decltype及函数返回类型后置
前言:在早期C/C++中auto关键字的作用是:一个存储类型指示符,使用auto修饰的变量,存储类型为自动存储期,从变量声明处生命周期开始,出变量所在代码块生命周期结束,并且 全局变量不能用auto修饰。但是局部变量的生命周期本来就是进入作用域生命周期开始,出作用域生命周期结束。 导致用auto修饰局部变量和不使用auto修饰没有任何区别,处于一个尴尬地步。
一、auto
1.1 C++ 11
C++11中,标准委员会赋予了auto全新的作用:auto做为类型占位符,auto声明的变量数据类型由编译器在编译时推导而得,所以使用auto声明变量时必须对其进行初始化,在编译阶段编译器根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
#include <iostream>
using namespace std;
int main()
{
int x = 10;
auto y = x; // 因为x为int类型,所以等同于 int y = x;
auto a = 'a'; // 因为a为char类型,所以等同于 char a = 'a';
auto b = 1; // 因为b为int类型,所以等同于 int b = 1;
auto c = 3.14; // 因为c为double类型,所以等同于 double c = 3.14;
// auto d; 错误,无法推导d是什么类型,导致不能分配内存空间
cout << "y:" << typeid(y).name() << endl;
cout << "a:" << typeid(a).name() << endl;
cout << "b:" << typeid(b).name() << endl;
cout << "c:" << typeid(c).name() << endl;
}
输出:
y:int
a:char
b:int
c:double
注意:使用auto对多个变量推导时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器按第一个变量类型进行推导,然后用推导出来的类型定义其他变量
auto a = 10, b = 20; // 正确
auto x = 1, y = 3.14; // 错误,第一个变量x类型为int,但y为double
1.1.1 推导规则
规则1: auto 会删除引用、const 限定符和 volatile 限定符
const int i = 1;
auto a = i; // auto推导类型为int,而不是const int
int j = 2;
int &ref = j;
auto b = ref; // ref为引用,auto推导会删除引用属性,auto推导类型为int,b类型为int
auto &c = ref; // ref为引用,auto推导类型为int,c类型为int&
规则2: auto推导指针类型时,auto
与auto*
没有任何区别。
const int i = 1;
auto a = &i; // auto推导类型为const int*,a类型为const int*
auto* b = &i; // auto推导类型为const int,b类型为const int*
规则3: auto推导目标对象是数组或函数时,会被推导为对应指针类型
void fun() {
std::cout << "test" << std::endl;
}
int arr[] = {1, 2, 3};
auto a = arr; // auto推导类型为int *
auto b = fun; // auto推导类型为void (*)()
规则4 auto推导列表表达式
注意:下面规则适用于C++ 17
- 直接使用列表初始化,列表中必须为单元素,否则编译错误,auto推导类型为该元素类型
- 用等号加列表初始化,列表中可以包含多个类型相同的元素,auto推导类型为
std::initializer_list<T>
,其中T为元素类型
auto a1{3}; // auto推导类型为int
auto a2{1, 3}; // 编译错误,不是单个元素
auto a3 = {1, 2, 3}; // auto推导类型为std::initializer_list<int>
auto a4 = {1, 1.1}; // 编译错误,列表内元素类型不同
1.1.2 auto不能使用的场景
1.1.2.1 auto 不能做为函数形参
int add(auto x, auto y) // 错误,auto不能做函数形参,编译器不知道如何为形参分配空间
{
return x + y;
}
1.1.2.2 auto 不能声明数组
auto arr[] = {1,2,3}; // 错误
1.2 C++ 14
1.2.1 函数返回值类型推导
C++ 14支持使用auto对函数返回值类型进行推导,但要确保所有的返回值类型是相同的。
auto add(int i,int j) // 正确
{
return i+j;
}
auto add(double i,double j) // 错误,0为int类型,i+j为double类型,返回值类型不相同
{
if(i < 0.0 || j < 0.0)
return 0;
else
return i + j;
}
1.2.2 lambda
C++14支持在lambda中使用auto作为形参以及返回值类型推导。
auto f = [](auto x, auto y) // lambda中使用auto对函数形参类型推导
{
return x + y;
};
auto ret1 = f(1, 2); // ret1 = 3
auto ret2 = f(1, 2.2); // ret2 = 3.2
auto f = [](auto &x) ->auto& // lambda可以使用auto推导返回值类型,或者auto&返回引用
{
return x;
};
int x = 1;
cout << "&x:" << &x << endl;
auto& ref = f(x);
cout << "&ref:" << &ref << endl; // &x 与 &ref有相同地址
1.3、C++ 17
1.3.1 非类型模版参数
C++ 17支持auto作为非类型模版参数的占位符,但推导出来的类型必须是符合非类型模版参数类型要求的,否则编译会错误。
template <auto N>
void f()
{
cout << N << endl;
}
f<1>(); // 正确
f<'a'>(); // 正确
f<3.14>(); // 错误,浮点类型不能作为非类型模版参数
二、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
int var;
const int&& fx();
struct A { double x; };
const A* a = new A();
decltype(fx()); // 匹配规则2,推导为 const int&&
decltype(var); // 匹配规则1,推导为 int
decltype(a->x); // 匹配规则1 推导为 double
decltype((a->x)); // 内部括号导致语句作为表达式而不是成员访问计算。匹配规则3,a->x是左值,并且a被const修饰,推导为const double&
int i;
int *j;
int n[10];
decltype(i=0); // i=0不是标识符,不匹配规则1,i=0后返回i,i为左值匹配规则3,推导为int&
decltype(0,i); // 逗号表达式返回最右边参数,i为左值匹配规则3,推导为int&
decltype(n[5]);// 返回数组第六个元素(左值),左值匹配规则3,推导为int&
decltype(static_cast<int&&>(i)); // 将i转化为将亡值,匹配规则3,推导为int&&
decltype(i++); // i++为右值,匹配规则4,推导为int
decltype(++i); // ++i为左值,匹配规则3,推导为int&
2.1.2 CV限定符推导
CV 限定符(CV-qualifiers)指的是 const 和 volatile 关键字。它们用于限定变量、对象、类型,使得编译器对这些对象的处理方式有所不同。
/*
通常情况下decltype( expression )推导的类型会同步expression的CV限定符。
但如果expression是未加括号的成员变量时,父对象表达式的CV限定符会被忽略。
*/
struct A{
int x;
};
const int i = 1;
decltype(i); // 推导为const int
const A* obj= new A(); // 使用const修饰
decltype(obj->x); // 匹配规则1,推导为int,const属性会忽略
decltype((obj->x)); // 匹配规则3,推导为const int&, const属性不会被忽略
2.1.3 示例
在日常中我们经常会用求和函数,如果指定了函数形参类型则不够通用,下面使用模版实现一个通用求和函数。
// C++11并不支持auto占位的函数返回类型进行推导,需要结合后置返回类型上的decltype说明符
template <class T1, class T2>
auto sum(T1 x, T2 y) -> decltype(x + y)
{
return x + y;
}
// C++14支持auto占位的函数返回类型进行推导,代码可简写为
template <class T1, class T2>
auto sum(T1 x, T2 y)
{
return x + y;
}
上面这个示例又会让人产生疑问,C++14中decltype的作用似乎又被auto取代了,但并不是,使用auto占位的函数返回类型推导时,如果期望返回类型是引用,但auto占位只能返回值类型。
// 下列代码在C++14 中测试
template <class T>
auto return_ref(T& x) // 根据上文auto推导规则1,auto会删除引用限定符,推导为T
{
return x;
}
template <class T> -> decltype(x)
auto return_ref(T& x) // 根据decltype推导规则3,x为左值,推导为T&
{
return x;
}
2.2 C++ 14
在C++14中,支持使用decltype(auto)
来进行类型推导,它实质是将表达式代入到auto
然后再用decltype
规则进行推导。注意decltype(auto)
必须单独声明,不能结合指针、引用以及CV限定符。
int i;
int &&f();
auto a1 = i; // 推导类型为int
decltype(auto) a2 = i; // 即decltype(i),推导类型为int
auto a3 = (i); // 推导类型为int
decltype(auto) a4 = (i); // 即decltype((i)),推导类型为int&
auto a5 = f(); // auto会删除引用,推导类型为int
decltype(auto) a6 = f(); // 即decltype(f()),根据decltype推导规则2和3,推导类型为int&&
auto a7 = {0, 1}; // 推导类型为 initializer_list<int>
decltype(auto) a8={0,1}; // 编译错误,{1,2}不是表达式
auto *a9 = &i; // 推导类型为int
decltype(auto) *a10 = &i; // 编译错误,decltype(auto)必须单独声明
在2.1.3示例中如果使用auto占位函数返回类型进行推导,如果期望返回值是引用需要结合后置返回类型上的decltype说明符,有了decltype(auto)
后代码可简写为
template <class T>
decltype(auto) return_ref(T &x) // 返回类型为T&
{
return x;
}
2.3 C++ 17
C++ 17支持decltype(auto)
作为非类型模版参数的占位符,但推导出来的类型必须是符合非类型模版参数类型要求的,否则编译会错误。
template <decltype(auto) N>
void f()
{
cout << N << endl;
}
int x = 1;
const int y = 10;
f<x>(); // 编译错误,x不是常量
f<y>(); // 正确,使用const修饰,y具有常属性
f<'a'>(); // 正确, 'a'为常量
三、函数返回类型后置(C++ 11)
C++ 11支持函数返回类型后置,使用auto
在返回类型位置进行占位,表示“返回类型将会稍后引出或指定”,->
后才是真正返回类型。
语法:
auto fun(arg...) -> ret_type
{
// TODO
}
3.1 配合函数模版
在C++ 11以前,如果有下面这个函数模版,那么函数返回类型应该写什么呢?
template<class T1, class T2>
??? sum(T1 x, T2 y)
{
return x + y;
}
可能一开始就想到使用decltype
去推导x+y的类型,写出下面代码,但是会编译错误。因为编译器在解析返回类型时还没有解析到参数部分,对x和y类型一无所知。
template<class T1, class T2>
decltype(x+y) sum(T1 x, T2 y)
{
return x + y;
}
下面代码则可以编译通过,先将nullptr
转换为T1和T2类型指针,然后解引用求和,decltype
类型推导时不会真正计算表达式,所以这里求和不会有问题。不过这种写法不易懂且代码不美观。
template <class T1, class T2>
decltype(*static_cast<T1 *>(nullptr) + *static_cast<T2 *>(nullptr)) sum(T1 x, T2 y)
{
return x + y;
}
在C++11中我们可以使用下面代码解决上面问题。
template<class T1, class T2>
auto sum(T1 x, T2 y) -> decltype(x+y)
{
return x + y;
}
3.2 返回复杂类型
当要返回复杂类型,例如返回函数指针类型时,使用函数返回类型后置写法比较简洁。
int f1(int x)
{
return x;
}
// 写法1,编译错误
int(*)(int) void f2()
{
return f1;
}
// 写法2,正确
typedef int(*ft)(int);
ft f2()
{
return f1;
}
// 写法3,正确
auto f2() -> int(*)(int)
{
return f1;
}