写点什么

深入解析 decltype 和 decltype(auto)

作者:爱分享
  • 2024-04-12
    广东
  • 本文字数:4084 字

    阅读完需:约 13 分钟

深入解析decltype和decltype(auto)

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

decltype(expr) var;
复制代码

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

cint 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)为intconst int& x2 = 2; // (2) decltype(x2)为const int&const Base b; b.x; // (3) decltype(b.x)为intint 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) intdecltype(func());		// (2) intdecltype(x1,x2);		// (3) int&decltype(x1,0);			// (4) intdecltype(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) intdecltype((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 并关注,或者扫描以下二维码关注,以便在内容更新时直接向您推送。


公众号:iShare爱分享


发布于: 刚刚阅读数: 5
用户头像

爱分享

关注

还未添加个人签名 2024-04-08 加入

还未添加个人简介

评论

发布
暂无评论
深入解析decltype和decltype(auto)_代码优化_爱分享_InfoQ写作社区