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推导指针类型时,autoauto*没有任何区别。

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[] = {123}; // 错误

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 推导规则

  1. 如果 expression 参数是未加括号到标识符或类成员访问,则 decltype(expression) 推导是 T。如果不存在此类实体或 expression 参数命名一组重载函数,则编译器将生成错误消息。
  2. 如果 expression 参数是对一个函数或一个重载运算符函数的调用,则 decltype(expression) 推导是函数的返回类型
  3. 如果 expression 参数是将亡值,则 decltype(expression) 推导是T&&类型。 如果 expression 参数是左值,则 decltype(expression) 推导是T&
  4. 除去上面情况,则 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;
}


  • 15
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值