说起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的更多相关文章

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

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

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

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

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

    在条款1中,我们已经了解了有关模板型别的推导的一切必要知识,那么也就意味着基本上了解了auto型别推导的一切必要知识. 因为,除了一个奇妙的例外情况,auto型别推导就是模板型别推导.尽管和模板型别推 ...

  4. Effective Modern C++ 条款4:掌握查看型别推导结果的方法

    采用何种工具来查看型别推导结果,取决于你在软件开发过程的哪个阶段需要该信息.主要研究三个可能的阶段:撰写代码阶段.编译阶段.运行时阶段. IDE编译器 IDE中的代码编译器通常会在你将鼠标指针选停止某 ...

  5. Effective Modern C++ ——条款7 在创建对象时注意区分()和{}

    杂项 在本条款的开头书中提到了两个细节性问题: 1.类中成员初始化的时候不能使用小括号. 如: class A { int a(0);//错误 }; 2.对于原子性类别的对象初始化的时候不能使用= 如 ...

  6. Effective Modern C++ ——条款6 当auto型别不符合要求时,使用带显式型别的初始化物习惯用法

    类的代理对象 其实这部分内容主要是说明了在STL或者某些其他代码的容器中,在一些代理类的作用下使得最后的返回值并不是想要的结果. 而他的返回值则是类中的一个容器,看下面的一段代码: std::vect ...

  7. Effective Modern C++ ——条款5 优先选择auto,而非显式型别声明

    条款5 对于auto ,他的好处不仅仅是少打一些字这么简单. 首先在声明的时候, 使用auto会让我们养成初始化的习惯: auto x;//编译不通过必须初始化. 再次对于auto而言,它可以让我们定 ...

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

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

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

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

随机推荐

  1. Nvelocity 语法

    原文:Nvelocity 语法 1,数字循环   #foreach($i in [0..9])     $i #end 2,dictionary 根据key获取value值 #set($key1=&q ...

  2. Python学习之--迭代器、生成器

    迭代器 迭代器是访问集合元素的一种方式.从对象第一个元素开始访问,直到所有的元素被访问结束.迭代器只能往前,不能往后退.迭代器与普通Python对象的区别是迭代器有一个__next__()方法,每次调 ...

  3. Eclipse Unable to install breakpoint in XXX 解决办法

    Debug 时偶尔会出现:Eclipse Unable to install breakpoint in XXX 情况一: 清除所有断点就行了,原因是断点打到注释上了. breakpoint 窗口: ...

  4. Java 生成pdf表格文档

    最近在工作做一个泰国的项目,应供应商要求,需要将每天的交易生成pdf格式的报表上传到供应商的服务器,特此记录实现方法.废话不多说,直接上代码: THSarabunNew.ttf该文件是泰国字体自行网上 ...

  5. LUOGU P3355 骑士共存问题(二分图最大独立集)

    传送门 因为骑士只能走"日"字,所以一定是从一个奇点到偶点或偶点到奇点,那么这就是一张二分图,题目要求的其实就是二分图的最大独立集.最大独立集=n-最大匹配. #include&l ...

  6. JS Math.sin() 与 Math.cos() 用法 (含圆上每个点的坐标)

    Math.sin(x)      x 的正玄值.返回值在 -1.0 到 1.0 之间: Math.cos(x)    x 的余弦值.返回的是 -1.0 到 1.0 之间的数: 这两个函数中的X 都是指 ...

  7. JS-jquery 获取当前点击的对象

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http ...

  8. sed应用 升级场景配置文件更新 指定行追加

    function addLine() { confFile=configuration.xml isExist=`cat ${confFile} | grep "<listen_ena ...

  9. Mybatis-SqlSessionFactoryBuilder,SessionFactory与SqlSession的并发控制

    SqlSessionFactoryBuilder 这个类可以被实例化,使用和丢弃.一旦你创建了 SqlSessionFactory 后,这个类就不需要存在了.因此 SqlSessionFactoryB ...

  10. 《数据结构与算法分析——C语言描述》ADT实现(NO.05) : 散列(Hash)

    散列(Hash)是一种以常数复杂度实现查找功能的数据结构.它将一个关键词Key,通过某种映射(哈希函数)转化成索引值直接定位到相应位置. 实现散列有两个关键,一是哈希函数的选择,二是冲突的处理. 对于 ...