Effective Modern C++ 条款3:理解decltype
说起decltype,这是个古灵精怪的东西。对于给定的名字或表达式,decltype能告诉你该名字或表达式的型别。一般来说,它告诉你的结果和你预测的是一样的。不过,偶尔它也会给出某个结果,让你抓耳挠腮,不得不 去参考手册或在线FAQ页面求得一些启发。
先从一般案例讲起——就是那些不会引发意外的案例。与模板和auto的型别推导过程相反,decltype一般只会鹦鹉学舌,返回给定的名字或表达式的确切型别而已:
const int i = ; //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 {
...
T& operator[](std::size_t index);
...
}; vector<int> v; //decltype(v)是vector<int>
...
if(v[] == ) ... //decltype(v[0])是int&
上面都是没有意外的案例。
C++11中,decltype的主要用途大概就在于声明那些返回值型别依赖于形参型别的函数模板。举个例子,假设我们想要撰写一个函数,其形参中包括一个容器,支持方括号下标语法(即“[]”)和一个下标,并会在返回下标操作结果前进行用户验证。函数的返回值型别须与下标操作结果的返回值型别相同。
一般来说,含有型别T的对象的容器,其operator[]会返回T&。std::deque就属于这种情况,而std::vector也几乎总是属于这种情况。只是std::vector<bool>对应deoperator[]并不返回bool&,而返回一个全新对象。至于这种处理的原因和具体处理结果,在条款6 中会有详细探讨。在此时此地,重要的在于容器的operator[]的返回型别取决于该容器本身。
而decltype使得这样的意思表达简单易行。下面是我们撰写该模板的首次尝试,其中演示了使用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 syntax),即该函数的返回值型别将在形参列表之后(在“->”之后)。尾序返回值的好处在于,在指定返回值型别时可以使用函数形参。比如,在authAndAccess中,我们在指定返回值型别时就可以使用c和i。如果我们还是使用传统的返回值型别先序语法,那c和i会由于还未声明,从而无法使用。
采用了这么一个声明形式以后,operator[]返回值是什么性别,authAndAccess的返回值就是什么型别,和我们期望的结果一致。
C++11允许对单表达式的lambda式的返回值型别实施推导,而C++14则将这个允许返回扩张到了一切lambda式和一切函数,包括那些多表达式的。对于authAndAccess这种情况来说,这就意味着在C++14中可以去掉返回值型别尾序语法,而只保留前导auto。在那样的声明形式中,auto确实说明会发生型别推导。具体地说,它说明编译器会依据函数实现来实施函数返回值的型别推导:
template<typename Container, typename Index> //C++14;
auto authAndAccess(Container& c, Index i) //不甚正确
{
authenticateUser();
return c[i]; //返回值型别是根据c[i]推导出来
}
条款2解释说,编译器会对auto指定为返回型别的函数实现模板型别推导。在上例中,这样就会留下隐患。一如前面讨论的那样,大多数含有型别T的对象的容器的operator[]会返回T&,但是条款1解释说,模板型别推导过程中,初始化表达式的引用性会被忽略。这将会对代码产生怎样的影响呢?
std::deque<int> d;
...
authAndAccess(d,) = ; //验证用户,并返回d[5],
//然后将其赋值为10;
//这段代码无法通过编译
此处,d[5]返回的是int&,但是对authAndAccess的返回值实施auto型别推导将剥去引用饰词,这么一来返回值型别将成了int。作为函数的返回值,该int是个右值,所以上述代码其实是尝试将10赋给一个右值int。这在C++中属于禁止的行为,所以代码无法通过编译。
欲让authAndAccess如我们期望般运作,就要对其返回值实施decltype型别推导,即指定authAndAccess的返回值型别与表达式c[i]返回的型别完全一致。C++的监护人们,由于预见到在某些型别推导时需要采用decltype型别推导规则,在C++14中通过decltype(auto)饰词解决了这个问题。乍看上去自相矛盾,其实完全和星河里:auto指定了欲实施推导的型别,而推导过程中采用的是decltype的规则。总而言之,我们可以这样撰写authAndAccess:
template<typename Container, typename Index> //C++14;
decltype<auto> //能够运行
authAndAccess(Container& c, Index i) //但仍需改进
{
authenticateUser();
return c[i];
}
现在,authAndAccess的返回值型别真的和c[i]返回的型别一致了。具体地说,一般情况下c[i]返回T&,authAndAccess也会返回T&。而对于少见情况,c[i]返回一个对象型别,authAndAccess也会亦步亦趋地返回对象型别。
decltype(auto)并不限于在函数返回值型别处使用。在变量声明的场合上,若你也想在初始化表达式处应用decltype型别推导规则,也可以照样便宜行事:
Widget w;
const Widget& cw = w; auto myWidget1 = cw; //auto型别推导: Widget1的型别是Widget
decltype(auto) myWidget2 = cw; //decltype型别推导:myWidget2的型别是const Widget&
再看一遍C++14版本的authAndAccess:
template<typename Container, typename Index>
decltype<auto> authAndAccess(Container& c, Index i) ;
容器的传递方式是对非常量的左值引用(lvalue-reference-to-non-const),因为返回该容器的某个元素的引用,就意味着允许客户对容器进行修改。不过这也意味着无法向该函数传递右值容器。右值是不能绑定到左值引用的(除非是对常量的左值引用,与本例情况不符)。
必须承认,想authAndAccess传递右值容器属于罕见情况。一个右值容器,作为一个临时对象,一般而言会在包含了调用authAndAccess的语句结束处被析构,而这就是说,该容器中某个元素的引用(这是authAndAccess一般情况下会返回的)会在创建它的那个语句结束时被置于空悬状态。但即使如此,想authAndAccess传递一个临时对象仍然可能是合理行为。客户可能就是想要制作该临时容器的某元素的一个副本,请看下例:
std::deque<std::string> makeStringDeque();//工厂函数 //制作makeStringDeque返回的deque的第5个元素的副本
auto s = authAndAccess(makeStringDeque(),);
容器的传递方式是对非常量的左值引用(lvalue-reference-to-non-const),因为返回该容器的某个元素的引用,就意味着允许客户对容器进行修改。不过这也就意味着无法向该函数传递右值容器。右值是不能绑定到左值引用的(除非是对常量的左值引用,与本例情况不符)。
如果要支持这种用法,就得修订authAndAccess的声明,以同时接受左值和右值。重载是个办法(一个重载版本声明一个左值引用形参,另一个重载版本声明一个右值引用形参),但这么一来就需要维护两个函数。避免这个后果的一个方法是让authAndAccess采用一种既能绑定到左值也能够绑定到右值的引用形参,而条款24给出了解释,说明这正是万能引用大显身手之处。这么一来,authAndAccess就可以这样声明:
template<typename Container, typename Index>
//c现在是个万能引用了
decltype<auto> authAndAccess(Container&& c, Index i) ;
在本模板中,我们对于操作的容器型别并不知情,同时对下标对象型别也一样不知情。对未知型别的对象采用值传递有着诸多风险:非必要的复制操作带来的性能隐患、对象截切(slicing)问题带来的行为异常(参见条款41)等,但是在容器下标这个特定问题上,遵循标准库中给出的下标值示例(例如std::string、std::vector和std::deque的operator[])应该是合理的,所以这里坚持使用了按值传递。
不过,我们需要更新该模板的实现,以使它与条款25所教导我们的内容相符:对万能引用要应用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++11终结版
auto
authAndAccess(Container&& c, Index i)
->decltype(std::forward<Container>(c)[i])
{
authenticateUser();
return std::forward<Container>(c)[i];
}
decltype几乎总会生成你期望的型别,蛋挞偶尔也会吓到你。实在地讲,除非你是重度的库实现者,你一般不太会遭遇这些规则的例外情况。
讲decltype应用于一个名字之上,就会得出改名字的声明型别。名字其实是左值表达式,但如果仅有一个名字,decltype的行为保持不变。不过,如果是比仅有名字更复杂的左值表达式的话,decltype就保证得出的型别总是左值引用。换言之,只要一个左值表达式不仅是一个型别为T的名字,他就得出一个T&型别。这种行为一般而言没有什么影响,因为绝大多数左值表达式都自带一个左值引用饰词。例如,返回左值的函数总是返回左值引用。
但这种行为还是会导致一个值得注意的后果,请看表达式:
int x = ;
其中x是一个变量名字,所以decltype(x)的结果是int。但是如果把名字x放入一对小括号中,就得到了一个比仅有名字更复杂的表达式“(x)”。作为一个名字,x是个左值,而在C++的定义中,表达式(x)也是一个左值,所以decltype((x))的结果就成了int&。仅仅把一个名字放入一对小括号,就改变了decltype的推导结果!
在C++11中,知道了这个,其实也就是满足一下猎奇心理而已,但如果和C++14对decltype(auto)的支持这么联合一下,一个看似无关紧要的返回值写法上的小改动,就会影响到函数的型别推导结果:
decltype(auto) f1()
{
int x = ;
...
return x; //decltype(x)是int,所以f1返回的是int
} decltype(auto) f2()
{
int x = ;
...
return (x); //decltype((x))是int&,所以f2返回的是int&
}
请注意,问题不仅仅在于f2和f1有着返回值型别的不同,更重要的是f2返回一个局部变量的引用!这种代码会把你送上未定义行为的快车——你永远不想乘坐的那种。
主要的教训在于,使用decltype(auto)时需要极其小心翼翼。看似是用以推导型别表达式的写法这样无关紧要的细节,却影响着decltype(auto)得出的结果。为了保证推导所得的型别和你期望的一致,请使用条款4讲述的技术。
同事,请勿因此丢到了大局观。没错,decltype(无论是单独使用,还是和auto配合使用)时不时地会得出意外的型别推导结果,但那说到底并非正常情形。在正常情形下,decltype产生的行呗就和你期望的一致。
以上的说法在decltype应用于名字时尤其成立,因为在那种情况下,decltype的行为可谓名副其实:它得出的就是该名字的声明型别(declared type)。
要点速记:
1、绝大多数情况下,decltype会得出变量或表达式的型别而不作任何修改。
2、对于型别为T的左值表达式,除非该表达式仅有一个名字,decltype总是得出型别T&。
3、C++14支持decltype(auto),和auto一样,它会从其初始化表达式出发来推导型别,但是它的型别推导使用的是decltype的规则。
Effective Modern C++ 条款3:理解decltype的更多相关文章
- Effective Modern C++ ——条款2 条款3 理解auto型别推导与理解decltype
条款2.理解auto型别推导 对于auto的型别推导而言,其中大部分情况和模板型别推导是一模一样的.只有一种特例情况. 我们先针对auto和模板型别推导一致的情况进行讨论: //某变量采用auto来声 ...
- [Effective Modern C++] Item 3. Understand decltype - 了解decltype
条款三 了解decltype 基础知识 提供一个变量或者表达式,decltype会返回其类型,但是返回的内容会使人感到奇怪. 以下是一些简单的推断类型: ; // decltype(i) -> ...
- Effective Modern C++ 条款2:理解auto型别推导
在条款1中,我们已经了解了有关模板型别的推导的一切必要知识,那么也就意味着基本上了解了auto型别推导的一切必要知识. 因为,除了一个奇妙的例外情况,auto型别推导就是模板型别推导.尽管和模板型别推 ...
- Effective Modern C++ 条款4:掌握查看型别推导结果的方法
采用何种工具来查看型别推导结果,取决于你在软件开发过程的哪个阶段需要该信息.主要研究三个可能的阶段:撰写代码阶段.编译阶段.运行时阶段. IDE编译器 IDE中的代码编译器通常会在你将鼠标指针选停止某 ...
- Effective Modern C++ ——条款7 在创建对象时注意区分()和{}
杂项 在本条款的开头书中提到了两个细节性问题: 1.类中成员初始化的时候不能使用小括号. 如: class A { int a(0);//错误 }; 2.对于原子性类别的对象初始化的时候不能使用= 如 ...
- Effective Modern C++ ——条款6 当auto型别不符合要求时,使用带显式型别的初始化物习惯用法
类的代理对象 其实这部分内容主要是说明了在STL或者某些其他代码的容器中,在一些代理类的作用下使得最后的返回值并不是想要的结果. 而他的返回值则是类中的一个容器,看下面的一段代码: std::vect ...
- Effective Modern C++ ——条款5 优先选择auto,而非显式型别声明
条款5 对于auto ,他的好处不仅仅是少打一些字这么简单. 首先在声明的时候, 使用auto会让我们养成初始化的习惯: auto x;//编译不通过必须初始化. 再次对于auto而言,它可以让我们定 ...
- Effective Modern C++翻译(4)-条款3:了解decltype
条款3 了解decltype decltype是一个有趣的东西,给它一个变量名或是一个表达式,decltype会告诉你这个变量名或是这个表达式的类型,通常,告诉你的结果和你预测的是一样的,但是偶尔的结 ...
- [C++11] Effective Modern C++ 读书笔记
本文记录了我读Effective Modern C++时自己的一些理解和心得. item1:模板类型推导 1)reference属性不能通过传值参数传入模板函数.这就意味着如果模板函数需要一个refe ...
随机推荐
- 总结windows cmd 查看进程,端口,硬盘信息
1.查看window所有进程 tasklist 2.查看windows所占用的进程号 tasklist|findstr 1916 3.杀死进程,进程pid taskkill /f /pid 10156 ...
- VMware Workstation 10 简体中文安装教程
分享到 一键分享 QQ空间 新浪微博 百度云收藏 人人网 腾讯微博 百度相册 开心网 腾讯朋友 百度贴吧 豆瓣网 搜狐微博 百度新首页 QQ好友 和讯微博 更多... 百度分享 分享到 一键分享 QQ ...
- tomcat下面部署多个项目
最近需要部署多个项目,我目前所知道的两种方法,第一种是一个tomcat部署一个项目,需要布置多个tomcat就可以部署多个项目.第二种就是一个tomcat下面部署多个项目. 首先我们先来说说两种的优劣 ...
- JS规则 给变量取个名字(变量命名) 必须以字母、下划线或美元符号开头;区分大小写;不允许使用JS关键字或保留字
给变量取个名字(变量命名) 我们为了区分盒子,可以用BOX1,BOX2等名称代表不同盒子,BOX1就是盒子的名字(也就是变量的名字). 我们赶快给变量取个好名字吧!变量名字可以任意取,只不过取名字要遵 ...
- 内核下枚举进程 (二)ZwQuerySystemInformation
说明: SYSTEM_INFORMATION_CLASS 的5号功能枚举进程信息.其是这个函数对应着ring3下的 NtQuerySystemInformation,但msdn上说win8以后ZwQu ...
- 《你不知道的JavaScript》上卷——第1章
知识点总结 1.什么是作用域? 作用域是根据名称查找变量的一套规则. 2.通常将JavaScript归类为“动态”或“解释执行”语言,但事实上它是一门编译语言. 3.编译 在传统编译语言的流程中,程序 ...
- sectionStorage与localStorage更新缓存,以及更新layui的数据缓存
var aa = sessionStorage.getItem('datInfo');//获取缓存数据 name = aa.user; var names = '张三'; sessionStorage ...
- HAVING方法也是连贯操作之一
HAVING方法也是连贯操作之一,用于配合group方法完成从分组的结果中筛选(通常是聚合条件)数据. having方法只有一个参数,并且只能使用字符串,例如: $this->field('us ...
- opencv-图像类型、深度、通道
转自:图像类型 与 opencv中图像基础(大小,深度,通道) 一.图像基本类型 在计算机中,按照颜色和灰度的多少可以将图像分为四种基本类型. 1. 二值图像 2. 灰度图像 3. 索引图像 4 ...
- CMS 源码解读
CMS 是”Content Management System” 的缩写,意为” 内容管理系统”. 内容管理系统是企业信息化建设和电子政务的新宠,也是一个相对较新的市场.对于内容管理,业界还没有一个统 ...