深入解析decltype和decltype(auto)

decltype关键字是C++11新标准引入的关键字,它和关键字auto的功能类似,也可以自动推导出给定表达式的类型,但它和auto的语法有些不同,auto推导的表达式放在“=”的右边,并作为auto所定义的变量的初始值,而decltype是和表达式结合在一起,语法如下:

decltype(expr) var;

它的语法像是函数调用,但它不是函数调用而是运算符,和sizeof运算符类似,在编译期间计算好,表达式expr不会被真正执行,因此不会产生汇编代码,如下的代码:

int func(int);
decltype(func());

func函数不会真正被调用,只会在编译期间获取他的类型。decltype和auto在功能上大部分相似,但推导规则和应用场景存在一些区别,如用auto定义变量时必须提供初始值表达式,利用初始值表达式推导出类型并用它作为变量的初始值,而decltype定义变量时可以不需要初始值。还有使用auto作为值语义的推导时,会忽略表达式expr的引用性和CV属性,而decltype可以保留这些属性,关于auto的详细解析,可以参考另一篇文章《深入解析C++的auto自动类型推导》

decltype在普通代码中应用并不广泛,主要用在泛型编程中较多,因此没有auto使用得多,下面将介绍decltype的推导规则,在介绍过程中遇到和auto规则不一样的地方则将两者对照说明,最后再介绍decltype无法被auto替代的应用场景。

推导规则

我将decltype的推导规则归纳为两条,根据expr有没有带小括号分为两种形式,如以下的形式:

decltype(expr)
// 或者
decltype((expr))
  • expr没有带括号的情形

当expr是单变量的标识符、类的数据成员、函数名称、数组名称时,推导出来的结果和expr的类型一致,并且会保留引用属性和CV修饰词,如下面的例子:

int func(int, int) {
    int x;
    return x;
}

class Base {
public:
	int x = 0;
};

int x1 = 1;		// (1) decltype(x1)为int
const int& x2 = 2;	// (2) decltype(x2)为const int&
const Base b;				
b.x;			// (3) decltype(b.x)为int
int a[10];		// (4) decltype(a)为int[10]
decltype(func);		// (5) 结果为int(int, int)

(1)式decltype(x1)的结果和x1的类型一致,为int类型。

(2)式的结果也是和x2一致,这里和auto的推导规则不同的是,它可以保留x2的引用属性和const修饰词,所以它的类型是const int&。

(3)式中定义的类对象b虽然是const的,但成员x的类型是int类型,所以结果也是int。

(4)和(5)都保留了原本的类型,这个也是和auto的推导结果不同的,使用auto推导的规则它们会退化为指针类型,这里则保留了它们数组和函数的类型。

当expr是一条表达式时,decltype(expr)的结果视expr表达式运算后的结果而定(在编译时运算而非运行时运算),当expr返回的结果是右值时,推导的结果和返回结果的类型一致,当expr返回的结果是左值时,推导的结果是一个引用,见下面的例子:

int x1 = 1;
int x2 = 2;
decltype(x1 + x2);	// (1) int
decltype(func());	// (2) int
decltype(x1,x2);	// (3) int&
decltype(x1,0);		// (4) int
decltype(a[1]);		// (5) int&

(1)式因为两个变量相加后返回一个数值,它是一个右值,所以推导结果和它的类型一致,这里换成加减乘除都是一样的。

(2)是一个函数调用,跟上面的使用函数名称不同,这里会调用函数(编译时),根据函数的返回结果来确定推导出来的类型,如果返回结果是引用或者指针类型,则推导结果也会引用或者指针类型,此函数返回的结果是int型,所以结果也是int型。

(3)和(4)是逗号表达式,它的返回结果是逗号后的那个语句,(3)是返回x2,它是一个变量,是一个左值,所以推导结果是int&,而(4)的返回结果是0,是一个右值,因此结果和它的类型一致。

(5)是访问数组中的元素,它是一个左值,因此推导结果是一个引用。

  • expr带括号的情形

当expr带上括号之后,它的推导规则有了变化,表达式加上括号后相当于去执行这条语句然后根据返回结果的类型来推导,见下面的例子:

class Base {
public:
	int x = 0;
};

int x1 = 1;
int x2 = 2;
const Base b;
b.x;
decltype((x1+x2)); 	// (1) int
decltype((x1));		// (2) int&
decltype((b.x));	// (3) const int&

(1)式中相加后的结果是一个右值,加上括号后依然是一个右值,因此推导结果是int。

(2)式中跟之前没有加括号的情况不一样,加上括号相当于是返回x1变量,因此是一个左值,推导结果是一个引用。

(3)式中也跟之前的结果不一样了,加上括号相当于返回类的数据成员x,因此是一个左值,推导结果是一个引用,但因为定义的类对象b是一个const对象,要保持它的内容不可被修改,因此引用要加上const修饰。

最后还有要注意一点的是,decltype和auto一样也可以和&和一起结合使用,但和auto的规则不一样,auto与&和结合表示定义的变量的类型是一个引用或者指针类型,而decltype则是保留这个符号并且和推导结果一起作为最终的类型,见下面的例子:

int x1 = 1;
auto *pi = &x1;		// (1) auto为int,pi为int*
decltype(&x1) *pp;	// (2) decltype(&x1)为int*,pp为int**

(1)式中的auto推导结果为int而不是int,要将pi定义为指针类型需要明确写出auto

(2)式的decltype(&x1)的推导结果为int,它会和定义中的(*pp前面的星号)结合在一起,因此最终的结果是int**。

decltype的使用场景

上面提到decltype和auto的一个区别就是使用auto必须要有一个初始值,而decltype在定义变量时可以不需要初始值,这在定义变量时暂时无法给出初始值的情况下非常有用,见下面的例子:

#include <map>
#include <string>

template<typename ContainerT>
class Object {
public:
    void init(ContainerT& c) { it_ = c.begin(); }
private:
    decltype(ContainerT().begin()) it_;
};

int main() {
    std::map<std::string, int> m;
    Object<std::map<std::string, int>> obj;
    obj.init(m);
}

在定义类的成员it_时还没有初始值,这时无法使用auto来推导它的类型,况且这里也无法使用auto来定义类的数据成员,因为现在还不支持使用auto来定义非静态的数据成员的,但使用decltype却是可以的。

还有一种情形是使用auto无法做到的,就是auto在使用值语义的推导规则的时候会忽略掉引用属性和CV修饰词,比如:

int i = 1;
const int& j = i;
auto x = j;	// auto的结果为int

这里x无法推导出和变量j一样的类型,你可能会说,如果要使用引用类型,那可以这样写:

const auto& x = j;	// auto的结果为int, x的类型const int&

但这又会带来其它的问题,这样定义出来的变量的类型永远都是const引用的类型,无法做到根据不同的表达式推导出相应的类型,如果使用decltype则可以做到:

int i = 1;
const int& j = i;
decltype(j) x = j;	// x的类型为const int&
decltype(i) y = i;	// y的类型为int

上面的代码使用decltype就可以根据不同的初始值表达式来推导出不同的结果。但你可能会觉得初始值表达式要在左右两边写上两遍,比较累赘,单个变量的还好,如果是个长表达式的话就会显得代码很冗余,也不优雅,比如:

int x = 1;
int y = 2;
double z = 5.0;
decltype(x + y + z) i = x + y + z;

如果上面的例子中表达式再长点就更难看也更麻烦了,幸好C++14标准提出了decltype和auto结合的功能,也就是decltype(auto)的用法。

decltype(auto)的使用解析

自动推导表达式的结果的类型

decltype(auto)的使用语法规则如下:

decltype(auto) var = expr;

它的意思是定义一个变量var,auto作为类型占位符,使用自动类型推导,但推导的规则是按照decltype的规则来推导。因此上面的代码可以这样来写:

decltype(auto) j = x + y + z;

它的用法跟使用auto一样,利用右边的表达式来推导出变量j的类型,但是推导规则使用的是decltype的规则。这对需要保持右边表达式的引用属性和CV修饰词时就非常有用,上面的代码可以改为:

int i = 1;
const int& j = i;
decltype(auto) x = j;	// x的类型为const int&
decltype(auto) y = i;	// y的类型为int

decltype(auto)用于推导函数返回值的类型

decltype(auto)可以用于推导函数返回值的类型,auto也可以用于推导函数的返回值类型,在讲解auto的那篇文章中就已讲解过。但auto有个问题就是会忽略掉返回值的引用属性,但如果你用auto&来推导返回值类型的话,那所有的类型都将是引用类型,这也不是实际想要的效果,有没有办法做到如果返回值类型是值类型时就推导出值类型,如果返回值类型是引用则推导出结果是引用类型?假设有一个处理容器元素的函数,它接受一个容器的引用和一个索引,函数处理完这个索引的元素之后再返回这个元素,一般来说,容器都有重载了“[]"运算符,但有的容器可能返回的是这个元素的值,有的可能返回的是元素的引用,如:

T& operator[](std::size_t index);
// 或者
T operator[](std::size_t index);

这时我们就可以用decltype(auto)来自动推导这个函数的返回值类型,函数的定义如下:

template<typename Container, typename Index>
decltype(auto) process(Container& c, Index i) {
    // processing
    return c[i];
}

当传进来的容器的operator[]函数返回的是引用时,则上面的函数返回的是引用类型,如果operator[]函数返回的是一个值时,则上面的函数返回的是这个值的类型。

decltype(auto)使用陷阱

最后,对于decltype(auto)能够推导函数返回值为引用类型这一点,需要提醒一下的是,小心会有下面的陷阱,如下面的函数:

decltype(auto) func() {
    int x;
    // do something...
    return x;
}

这里推导出来的返回值类型是int,并且会拷贝局部变量x的值,这个没有问题。但如果是这样的定义:

decltype(auto) func() {
    int x;
    // do something...
    return (x);
}

这个版本返回的是一个引用,它将引用到一个即将销毁的局部变量上,当这个函数返回后,所返回的引用将引用到一个不存在的变量上,造成引用空悬的问题,程序的结果将是未知的。无论是有意的还是无意的返回一个引用,都要特别小心。

此篇文章同步发布于我的微信公众号:深入解析decltype和decltype(auto)

如果您感兴趣这方面的内容,请在微信上搜索公众号iShare爱分享或者微信号iTechShare并关注,或者扫描以下二维码关注,以便在内容更新时直接向您推送。

热门相关:四爷又被福晋套路了   剑道邪尊   龙皇武神   美食萌后:皇上,喂不饱!   异界之极品奶爸