现代C++之理解decltype

decltype用于生成变量名或者表达式的类型,其生成的结果有的是显而易见的,可以预测的,容易理解,有些则不容易理解。大多数情况下,与使用模板和auto时进行的类型推断相比,decltype作用于变量名或者表达式只是重复了一次变量名或者表达式的确切类型:

const int i = 0;                         // decltype(i) 为 const int
bool f(const Widget& w); // decltype(w) 为 const Widget&
// decltype(f) 为 bool(const Widget&)
struct Point {
int x, y; // decltype(Point::x) 为 int
}; // decltype(Point::y) 为 int
Widget w; // decltype(w) 为 Widget
if (f(w)) … // decltype(f(w)) 为 bool template<typename T> // std::vector 的简易实现
class vector {
public:

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

};
vector<int> v; // decltype(v) 为 vector<int>

if (v[0] == 0) … // decltype(v[0]) 为 int&

上面的结果都在意料之中,很好理解。C++11中,decltype的主要用于声明模板函数,此模板函数的返回值类型依赖于其参数类型。例如,看一个例子:我们需要实现一个模板函数,此模板函数的参数包括一个支持方括号("[]")索引的容器加一个int索引值,中间需要做一些验证操作,最后函数返回类型应该同容器索引操作的返回类型相同。

一个元素类型为T的容器,operator []的返回值类型应该为T&。std::queue容器都满足这个要求,std::vector大部分情况下都满足(std::vector<bool>为一个例外,operator[]并不返回bool&,而是一个全新的对象),因此注意这里的容器操作符operator[]的返回值类型依赖于容器类型。

使用decltype可以很方便的实现此模板函数,此模板需要做一些改进,后面讨论:

template<typename Container, typename Index> // 此函数可以工作,但可以改进。
auto authAndAccess(Container& c, Index i)
-> decltype(c[i])
{
authenticateUser();
return c[i];
}

注意这里的auto并没有做任何类型推断,只是用来表明这里使用的是C++11 的拖尾返回类型(trailing return type)语法,也就是函数返回类型将在参数列表之后进行声明(在"->"之后),优点是可以使用函数参数来声明函数返回类型(如果将返回类型放置于函数之前,这里的参数c和i还没有被声明,因此不能被使用)。

C++14中可以忽略拖尾返回类型了,这样上面的实现就只剩下auto了。使用这种形式的声明就意味着要进行类型推断。编译器将会根据函数的实现来推断函数返回类型:

template<typename Container, typename Index> // C++14,但是不正确
auto authAndAccess(Container& c, Index i)
{
authenticateUser();
return c[i];//根据c[i]推断返回类型
}

上一边帖子的最后解释了,使用auto作为函数返回类型,编译器将会使用模板类型推断推断返回类型。这种情况下上面的函数就有问题了。对于大多数元素类型为T的容器,operator[]返回T&,但是模板类型推断

中解释了,用于初始化的表达式的引用属性会被忽略掉。看下面的代码:

std::deque<int> d;

authAndAccess(d, 5) = 10; // 返回 d[5],赋值10,编译会出错

这里的d[5]会返回int&,但是authAndAccess中的auto返回类型推断将会把引用剔除掉,最后的返回值类型为一个右值int。C++中禁止将10赋值给一个右值int,因此编译失败。

为了得到我们想要的,也就是不使用拖尾返回类型,我们需要对返回类型使用decltype类型推断,也就是要指定函数authAndAccess和表达式c[i]返回相同的类型。C++14中我们使用decltype(auto)标志符来达到目的。它的意义是:auto表明要进行类型推断,decltype说明推断过程中将会使用decltype推断规则。最后实实现autoAndAccess如下:

template<typename Container, typename Index> // C++14,正确的实现,仍然可以改进
decltype(auto) authAndAccess(Container& c, Index i)
{
authenticateUser();
return c[i];//根据c[i]推断返回类型
}

现在authAndAccess将会返回c[i]所返回的。如果c[i]返回一个T&,authAndAccess也会返回T&。如果c[i]返回一个对象,authAccess也会返回一个对象。

decltype(auto)的使用并不限制于函数返回类型,也能够用于变量的声明:

Widget w;
const Widget& cw = w;
auto myWidget1 = cw; // auto 类型推断,myWidget1的类型为 Widget,引用和const属性被忽略掉了
decltype(auto) myWidget2 = cw;//myWidget2的类型为const Widget& ,因为这里使用了decltype推断推着

在authAndAccess的最后一个版本中,我们提到了此函数仍然可以改进,如何做呢?再看一眼函数声明:

template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i);

这里函数参数为按指向非const左值的引用进行传递,返回容器中元素的引用到客户端就允许客户端对其进行修改。既然是左值引用我们就不能够向这个函数传递右值。但是传递右值到函数中可能是有意义的,客户端可能只想获得容器中元素的一份拷贝,看下面的例子:

std::deque<std::string> makeStringDeque(); // 工厂函数
// 获取从makeStringDeque中返回的deque中第五个元素的拷贝
auto s = authAndAccess(makeStringDeque(), 5);

因此我们需要对函数进行修订,使此函数即能够接受左值,也能接受右值。可以使用重载(一个函数声明一个左值引用参数,一个函数声明一个右值引用参数),但是需要维护两个函数。我们可以使用universal reference参数类型来避免这种情况,因为 此参数类型即可以绑定到右值,也可以绑定到左值,最后authAndAccess可以声明成下面这个样子:

template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i);

在这个模板函数中,我们不知道需要操作的容器类型,也当然不知道容器内的元素类型,对一个不了解其类型的对象采用按值传递,可能会带来不必要的拷贝造成的性能问题,还有可能有对象切片问题,但是这里我们使用容器索引获取函数返回值,仿照标准模板库中的实例来实现看上去是合理的(例如,std::string,std::vector,std::deque,),因此我们坚持使用按值传递。

为了从返回值中传递右值属性,我们需要对univversal reference使用std::forward:

template<typename Container, typename Index> // C++14,最终版本
decltype(auto) authAndAccess(Container&& c, Index i)
{
authenticateUser();
return std::forward<Container>(c)[i];
}

上面的函数需要使用C++ 14的编译器,如果没有,也可以使用C++11中的模板版本,与C++ 14不同的是需要你自己指定返回类型:

template<typename Container, typename Index> // C++14,最终版本
decltype(auto) authAndAccess(Container&& c, Index i) ->decltype(std::forward<Container>(c)[i])
{
authenticateUser();
return std::forward<Container>(c)[i];
}

我们还需要说明另外一个问题,文章开始提及了,decltype大多数情况下会返回你所期望的类型,但是还有一些例外,为了更好的理解decltype,我们也需要熟悉这些情况。

将decltype应用于变量名会生成同此变量名相同的类型。这种情况下没有例外。但是对于左值表达式来说情况就有些复杂了,decltype会确保其作用于左值表达式时,生成的类型为一个左值引用。也就是说如果一个左值表达式(而非变量名)的类型为T,那么decltype(左值表达式)的类型就是T&。大多数情况下这不会有任何影响,因为大多数左值表达式都会显示的包含一个左值引用标识符。例如,函数返回左值时,通常会返回左值引用,也就包含一个&标识符。

但是有一种情况需要注意:

int x =0;

x是变量名,因此decltype(x)的类型为int。但是用括号()将x括起来将会生成一个表达式,表达式(x)也为左值,因此decltype((x))为int&。

进一步考虑c++14中的decltype(auto):

decltype(auto) f1()
{
int x = 0;

return x; // decltype(x) 为int,f1返回int
}
decltype(auto) f2()
{
int x = 0;

return (x); // decltype((x)) 为 int&, f2 返回 int&
}

注意第二种情况不仅仅返回值发生了变化,而且返回的是指向本地变量的引用。因此要警觉这种错误的发生。

最后总结一下:

  • 大多数情况下decltype为变量名或者表达式生成的类型是不会发生变化的。
  • decltype作用于左值表达式时,生成的类型为T&。
  • 采用C++14中的decltype(audo)进行类型推断时,使用decltype推断规则进行推断。

现代C++之理解decltype的更多相关文章

  1. item 3: 理解decltype

    本文翻译自modern effective C++,由于水平有限,故无法保证翻译完全正确,欢迎指出错误.谢谢! 博客已经迁移到这里啦 decltype是一个奇怪的东西.给出一个名字或者一个表达式,de ...

  2. Effective Modern C++ ——条款2 条款3 理解auto型别推导与理解decltype

    条款2.理解auto型别推导 对于auto的型别推导而言,其中大部分情况和模板型别推导是一模一样的.只有一种特例情况. 我们先针对auto和模板型别推导一致的情况进行讨论: //某变量采用auto来声 ...

  3. Effective Modern C++ 条款3:理解decltype

    说起decltype,这是个古灵精怪的东西.对于给定的名字或表达式,decltype能告诉你该名字或表达式的型别.一般来说,它告诉你的结果和你预测的是一样的.不过,偶尔它也会给出某个结果,让你抓耳挠腮 ...

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

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

  5. Effective Modern C++翻译(4)-条款3:了解decltype

    条款3 了解decltype decltype是一个有趣的东西,给它一个变量名或是一个表达式,decltype会告诉你这个变量名或是这个表达式的类型,通常,告诉你的结果和你预测的是一样的,但是偶尔的结 ...

  6. [C++11] Effective Modern C++ 读书笔记

    本文记录了我读Effective Modern C++时自己的一些理解和心得. item1:模板类型推导 1)reference属性不能通过传值参数传入模板函数.这就意味着如果模板函数需要一个refe ...

  7. modern effective C++ -- Deducint Types

    1. 理解模板类型推导 1. expr是T& template<typename T> void f(T & param); // 我们声明如下变量 int x = 27; ...

  8. c++11-17 模板核心知识(十一)—— 编写泛型库需要的基本技术

    Callables 函数对象 Function Objects 处理成员函数及额外的参数 std::invoke<>() 统一包装 泛型库的其他基本技术 Type Traits std:: ...

  9. 深入理解C++中的mutable,using,decltype等关键字

    深入理解C++中的mutable关键字 mutable的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词. 在C++中,mutable也是为了突破const的限制而设 ...

随机推荐

  1. isinstance和issubclass,__getattribute__,__getitem__,__setitem__,delitem__,__str__(三十五)

    isinstance(obj,cls)检查是否obj是否是类 cls 的对象 issubclass(sub, super)检查sub类是否是 super 类的派生类 class Foo: def __ ...

  2. javascript面向对象精要第四章构造函数和原型对象整理精要

  3. plink, vcftool计算等位基因频率(allele frequency,vcf)

    计算等位基因频率有两种方式,第一种用vcftool计算: /path/to/vcftools --vcf file.vcf --freq --chr 1 --out filefreq 很简单的一个命令 ...

  4. Android Support Library 是什么?

    这两天刚开始学习安卓,这里记录下这两天遇到的一些小问题. 首先先贴一个安卓 API 等级. 官方地址:https://developer.android.com/about/dashboards/ ( ...

  5. 3分钟学会sessionStorage用法

    前言: 因最近移动端开发过程中遇到一个运营提出的所谓技术难点需求,对于原生APP来说轻而易举,毕竟自己的APP用户操作指哪打哪,但是H5该怎么做?H5就实现不了么?对于一个爱研究攻克这些前端棘手问题的 ...

  6. 2156: 中南大学2018年ACM暑期集训前期训练题集(入门题) D: 机器人的指令

    不要用gets!不要用gets!不要用gets! 不要用gets!不要用gets!不要用gets! 不要用gets!不要用gets!不要用gets! 不要用gets!不要用gets!不要用gets! ...

  7. Arcgis api 离线部署

    Arcgis api 离线部署 修改 文件一(init.js)位置:arcgis_js_v317_api\arcgis_js_api\library\3.17\3.17\init.js 将[HOSTN ...

  8. conda常用命令总结

    conda 一些背景历史以及如何安装这里就不说了,因为实在是漫天都在飞,随便都能找到相关的资料.我这里只是将平时常用到的 Conda 命令进行汇总,以备不时之需,因为我也是一个忘性极大的人,实在是记不 ...

  9. Kafka 0.10 DelayedTaskQueue的用法和实现

    DelayedTaskQueue 是在ConsumerNetworkClient类中使用,是Kafka自己实现的一个很重要的数据结构. 官方解释:延时队列,Tracks a set of tasks ...

  10. unity引用查找插件-ReferenceFinder

    简介   这是一个用来查找资源引用和依赖的插件,通过缓存来保存资源间的引用信息,通过树状结构直观的展示.   由于是通过缓存进行实现的,所以在希望的到精确的引用信息时需要刷新缓存.不过由于缓存的存在, ...