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

  1. decltype(expr) var;

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

  1. int func(int);
  2. decltype(func());

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

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

推导规则

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

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

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

  1. int func(int, int) {
  2. int x;
  3. return x;
  4. }
  5. class Base {
  6. public:
  7. int x = 0;
  8. };
  9. int x1 = 1; // (1) decltype(x1)为int
  10. const int& x2 = 2; // (2) decltype(x2)为const int&
  11. const Base b;
  12. b.x; // (3) decltype(b.x)为int
  13. int a[10]; // (4) decltype(a)为int[10]
  14. 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返回的结果是左值时,推导的结果是一个引用,见下面的例子:

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

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

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

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

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

  • expr带括号的情形

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

  1. class Base {
  2. public:
  3. int x = 0;
  4. };
  5. int x1 = 1;
  6. int x2 = 2;
  7. const Base b;
  8. b.x;
  9. decltype((x1+x2)); // (1) int
  10. decltype((x1)); // (2) int&
  11. decltype((b.x)); // (3) const int&

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

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

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

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

  1. int x1 = 1;
  2. auto *pi = &x1; // (1) auto为int,pi为int*
  3. 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在定义变量时可以不需要初始值,这在定义变量时暂时无法给出初始值的情况下非常有用,见下面的例子:

  1. #include <map>
  2. #include <string>
  3. template<typename ContainerT>
  4. class Object {
  5. public:
  6. void init(ContainerT& c) { it_ = c.begin(); }
  7. private:
  8. decltype(ContainerT().begin()) it_;
  9. };
  10. int main() {
  11. std::map<std::string, int> m;
  12. Object<std::map<std::string, int>> obj;
  13. obj.init(m);
  14. }

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

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

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

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

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

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

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

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

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

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

decltype(auto)的使用解析

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

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

  1. decltype(auto) var = expr;

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

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

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

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

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

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

  1. T& operator[](std::size_t index);
  2. // 或者
  3. T operator[](std::size_t index);

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

  1. template<typename Container, typename Index>
  2. decltype(auto) process(Container& c, Index i) {
  3. // processing
  4. return c[i];
  5. }

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

decltype(auto)使用陷阱

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

  1. decltype(auto) func() {
  2. int x;
  3. // do something...
  4. return x;
  5. }

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

  1. decltype(auto) func() {
  2. int x;
  3. // do something...
  4. return (x);
  5. }

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

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

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

深入解析decltype和decltype(auto)的更多相关文章

  1. c++11-17 模板核心知识(九)—— 理解decltype与decltype(auto)

    decltype介绍 为什么需要decltype decltype(auto) 注意(entity) 与模板参数推导和auto推导一样,decltype的结果大多数情况下是正常的,但是也有少部分情况是 ...

  2. [Effective Modern C++] Item 3. Understand decltype - 了解decltype

    条款三 了解decltype 基础知识 提供一个变量或者表达式,decltype会返回其类型,但是返回的内容会使人感到奇怪. 以下是一些简单的推断类型: ; // decltype(i) -> ...

  3. 解剖Nginx·自动脚本篇(1)解析配置选项脚本 auto/options

    在安装Nginx之前(即运行make脚本之前),首先是进行安装的配置准备,包括环境检查及生成文件.这些工作是由自动脚本完成的.和绝大多数软件一样,Nginx的自动脚本的入口,同样是名为configur ...

  4. C++ 11 学习1:类型自动推导 auto和decltype

    Cocos 3.x 用了大量的C++ 11 的东西,所以作为一个C++忠实粉丝,有必要对C++ 11进行一个系统的学习. 使用C++11之前,一定要注意自己使用的编译器对C++11的支持情况,有些编译 ...

  5. C++11 auto and decltype

    1.auto关键字 C++新标准引入auto关键词,此auto与之前C语言的auto意义已经不一样了. 这里的auto是修饰未知变量的类型,编译器会通过此变量的初始化自动推导变量的类型. 例如:aut ...

  6. auto 和 decltype

    一, auto 1, auto的作用     一般来说, 在把一个表达式或者函数的返回值赋给一个对象的时候, 我们必须要知道这个表达式的返回类型, 但是有的时候我们很难或者无法知道这个表达式或者函数的 ...

  7. c++11——auto,decltype类型推导

    c++11中引入了auto和decltype关键字实现类型推导,通过这两个关键字不仅能够方便的获取复杂的类型,而且还能简化书写,提高编码效率.     auto和decltype的类型推导都是编译器在 ...

  8. C++ 11常见功能介绍:auto,decltype,nullptr,for,lambda

    什么是C++11 C++11是曾经被叫做C++0x,是对目前C++语言的扩展和修正,C++11不仅包含核心语言的新机能,而且扩展了C++的标准程序库(STL),并入了大部分的C++ Technical ...

  9. auto和decltype的用法总结

    一, auto 1, auto的作用     一般来说, 在把一个表达式或者函数的返回值赋给一个对象的时候, 我们必须要知道这个表达式的返回类型, 但是有的时候我们很难或者无法知道这个表达式或者函数的 ...

  10. C++ 处理类型名(typedef,auto和decltype)

    随着程序越来越复杂,程序中用到的类型也越来越复杂,这种复杂性体现在两个方面.一是一些类型难于"拼写",它们的名字既难记又容易写错,还无法明确体现其真实目的和含义.二是有时候根本搞不 ...

随机推荐

  1. Educational Codeforces Round 145 (Rated for Div. 2)C. Sum on Subarrays(构造)

    很意思的一道构造题 题意:给一个\(n.k\),让构造长度为n的数组满足,子数组为整数的个数为k个,负数的为\(k-(n+1)* n/2\),每个数的范围为\([-1000,1000]\) 这种构造题 ...

  2. git 常见命令和资源

    git练习 常用git清单 强制切换分支所指位置 git branch -f main c3强制分支main指向c3 git branch -f main HEAD~3强制分支main指向head的父 ...

  3. 【思维题、KMP】P3526 [POI2011]OKR-Periodicity 题解

    P3526 [POI2011]OKR-Periodicity 题解 前言 一道非常厉害的思维题.看题解得到了一些提示搞出来了. 作为 2011 年的题还是很厉害的. 约定 定义 \(s[l,r]\) ...

  4. mybatis缓存源码解析

    为什么使用缓存 减少和数据库交互次数,提高执行效率 mybatis的缓存 mybatis一级缓存,也就是局部的sqlSession级别的缓存,默认是开启的 每一个 session 会话都会有各自的缓存 ...

  5. 在Linux平台使用wps卡顿现象解决方法

    是因为软件占据的内存过多,需要关掉目前不使用的软件,以释放系统内存.

  6. 基于python源码的啸叫抑制算法解析

    一 原理解析 从下图一中可以看出,该算法的原理也是先检测出来啸叫,然后通过陷波器来进行啸叫抑制的,和笔者以前分析的所用方法基本耦合. ​   二 源码分析   函数PAPR:计算峰值功率和平均功率的比 ...

  7. ESP8266 SPI 开发之软硬基础分析

    一 什么是SPI接口? SPI是一种高速.高效率的串行接口技术.通常由一个主模块和一个或多个从模块组成,主模块选择一个从模块进行同步通信,从而完成数据的交换.SPI是一个环形结构,通信时需要至少4根线 ...

  8. 逆向通达信Level-2 续六 (调试pad控件)

    调试终端面版单元, 以及宿主窗口 调试大数据面版单元, 以及宿主窗口 逆向通达信Level-2 续十一 (无帐号登陆itrend研究版) 逆向通达信Level-2 续十 (trace脱壳) 逆向通达信 ...

  9. 使用gitee

    git全局设置 git config --global user.name "张xx" git config --global user.email "xxx@qq.co ...

  10. 【Atcoder F - Cumulative Cumulative Cumulative Sum】线段树

    要特别注意下精度,long,int范围.WA了几次 import java.util.Scanner; class Main { // static long[] A2 ;//i^2*AI // st ...