05:优先使用auto,而非显示类型声明

显示类型声明有下面一些缺点:

int x; //未初始化,或者初始化为0,视语境而定

template<typename It>
void dwim(It b, It e) {
while (b != e) {
typename std::iterator_traits<It>::value_type //啰嗦
currValue = *b;

}
}

另外,如果想要使用闭包的类型来声明变量,但是闭包的类型只有编译器知道。

有了auto之后,上面这些缺点都可以解决:

int x1; // 潜在的未初始化风险
auto x2; // 编译错误!必须初始化
auto x3 = ; // fine template<typename It> // as before
void dwim(It b, It e) {
while (b != e) {
auto currValue = *b;

}
} auto derefUPLess =
[](const std::unique_ptr<Widget>& p1, const std::unique_ptr<Widget>& p2)
{ return *p1 < *p2; };

使用auto声明变量,必须初始化。

或许你认为没必要使用auto来声明变量持有闭包,使用std::function也可以:

std::function<bool(const std::unique_ptr<Widget>&, const std::unique_ptr<Widget>&)>
derefUPLess = [](const std::unique_ptr<Widget>& p1, const std::unique_ptr<Widget>& p2)
{ return *p1 < *p2; };

抛开词法上的啰嗦不谈,使用std::function和auto还是有所不同。使用auto声明的变量,类型与闭包一致,从而其要求的内存量和闭包也一样;使用std::function声明的变量,不管给定的签名如何,其都占有固定尺寸的内存,而这个尺寸对于其存储的闭包而言不一定够用,这种情况下,std::function就会使用堆内存来存储闭包。因此,std::function对象一般都会比auto声明的变量使用更多的内存;而且std::funciton的实现细节一般会限制内联,并产生间接函数调用,因此,通过std::function来调用闭包几乎必然会比通过auto声明的变量调用同一闭包来的要慢。所以,这种情况下std::funciton比auto又大又慢。

下面的代码:

std::vector<int> v;
unsigned sz = v.size();

标准规定v.size()的返回值类型是std::vector<int>::size_type,它是一个无符号整数,因此很多人就直接写成上面那样了。在32位Windows上,unsigned和std::vector<int>::size_type的内存尺寸是一样的,但是到了64位Windows上,unsigned是32位,而std::vector<int>::size_type则是64位,这就有可能导致异常行为。使用auto就不会有这样的麻烦:auto sz = v.size();

下面的代码:

std::unordered_map<std::string, int> m;
for (const std::pair<std::string, int>& p : m) {
… // do something with p
}

上面的代码看似合理,但是,std::unordered_map的value_type实际类型是std::pair<const Key, T>(std::map也一样),因此m中的std::pair的类型实际上是std::pair<const std::string, int>,但是上面的循环中,p的类型不是这样的,因此编译器就需要把m中的每个对象做一次复制操作,形成一个p可以绑定的临时对象,在循环的每次迭代结束时再析构该临时对象。使用auto就不会有这样的麻烦:

for (const auto& p : m) {
… // as before
}

这样使用auto不仅效率更高,而且打字也更少;犹有进者,如果对p取地址,则取得的一定是m中某个元素的地址,而不使用auto的版本,取得的则是临时对象的地址,而且该临时对象在循环迭代结束时会被析构。

最后,auto类型可以随着其初始化表达式的类型变化而自动变化,这意味着使用auto,某些重构动作就顺手做掉了,比如某个函数之前返回int,后来又觉得long更合适一些,如果函数返回结果存储在auto声明的变量中,就无需在调整变量的类型了。

06:当auto推导出非预期类型时应当使用显式的类型初始化

某些情况下,auto的类型推导会和你想的南辕北辙。举一个例子,下面的features函数接受一个Widget,返回一个std::vector<bool>,其中每个bool标识Widget是否支持某种特性:

std::vector<bool> features(const Widget& w);

Widget w;
bool highPriority = features(w)[]; // is w high priority?
processWidget(w, highPriority); // process w in accord with its priority

这份代码没有任何问题。但是如果我们改用auto:

auto highPriority = features(w)[]; // is w high priority?
processWidget(w, highPriority); // undefined behavior!

代码虽然可以编译,但是调用processWidget却是未定义行为。这是因为这种情况下,highPriority的类型已经不是        bool 了。尽管std::vector<bool>是bool的容器,但是对std::vector<bool>的operator[]操作,并不是返回容器中元素的引用(std::vector::operator[]对所有类型都返回引用,除了bool)。事实上,它返回的是一个std::vector<bool>::reference对象(一个在std::vector<bool>中内嵌的class)。

之所以要弄出一个std::vector<bool>::reference,是因为std::vector<bool>做过特化,用一种压缩形式表示其持有的bool元素,每个bool元素用一个bit来表示。因为std::vector<T>的operator[]应该返回一个T&,但是C++禁止bits的引用。没办法返回一个bool&,因此std::vector<T>的operator[]就返回一个行为上和bool&相似的对象std::vector<bool>::reference,该对象在任何bool适用的场合都表现的和bool一样,它通过隐式转换成bool来实现这一点。所以,下面的代码:

bool highPriority = features(w)[];

std::vector<bool>::reference隐式转换成了bool,以便能初始化highPriority,因而highPriority便持有了std::vector<bool>中第5位的值。

但是换成auto之后,highPriority的类型就成了std::vector<bool>::reference类型。而highPriority的值也视std::vector<bool>::reference的实现而定。std::vector<bool>::reference的实现方式可能是内部包含一个指针,指针指向的内存包含相应的位信息。这种情况下,features函数返回了一个临时的std::vector<bool>,暂时称其为temp,在temp上执行operator[]操作,返回的std::vector<bool>::reference中包含一个指针指向temp的内部数据,赋值给highPriority后,highPriority也持有一个指针指向相同的地址。但是在表达式的最后,临时对象temp被销毁,highPriority内部的指针就成了悬空空悬指针。从而导致调用processWidget成了一种未定义行为。

实际上,std::vector<bool>::reference仅仅是代理类的一个例子而已,一个通用的法则就是,“隐形”代理类不能和auto和平共处,因为代理类对象的生命周期一般设计为不会超过单条语句,所以要避免下面这种代码形式:

auto someVar = expression of "invisible" proxy class type;

如何知道代理类的存在呢?一般情况下,可以在库文档中找到代理类的说明,文档不够用时,也可以去看头文件。比如,std::vector<bool>::operator[]的代码如下:

namespace std { // from C++ Standards
template <class Allocator>
class vector<bool, Allocator> {
public:

class reference { … };
reference operator[](size_type n);

};
}

一旦 auto 被推导为代理类的类型而不是它被代理的类型时就有可能出现问题,auto本身没有问题。解决方案是强制进行类型转换,我把这种方法叫做显式的类型初始化原则。

显式类型初始化原则依然使用auto声明变量,但是要对初始化表达式进行强制类型转换。比如:

auto highPriority = static_cast<bool>(features(w)[]);

这里, features(w)[5] 还是返回一个 std::vector<bool>::reference 的对象,但是强制类型转换将表达式的类型转换成了bool,从而auto将highPriority推导为bool。

这种用法不限于会产生代理类对象的初始化物。它同样可以应用于你想要强调你意在创建一个类型有别于初始化表达式类型的变量的场合,比如下面的代码:

double calcEpsilon();
float ep = calcEpsilon(); // impliclitly convert double to float

尽管calcEpsilon返回double,但是你认为float的精度已经够用了,所以你用一个float变量ep存储返回值。如果使用auto,则可以:

auto ep = static_cast<float>(calcEpsilon());

Effective Modern C++:02auto的更多相关文章

  1. Effective Modern C++:04智能指针

    裸指针有着诸多缺点:裸指针的声明中看不出它指向的是单个对象还是数组:裸指针的声明中也无法看出使用完它指向的对象后是否需要删除,也就是声明中看不出裸指针是否拥有其指向的对象:即使知道要析构裸指针指向的对 ...

  2. Effective Modern C++:03转向现代C++

    07:在创建对象时注意区分()和{} 自C++11以来,指定初始化值的的方式包括使用小括号,等号,以及大括号: ); // initializer is in parentheses ; // ini ...

  3. Effective Modern C++:01类型推导

    C++的官方钦定版本,都是以ISO标准被接受的年份命名,分别是C++98,C++03,C++11,C++14,C++17,C++20等.C++11及其后续版本统称为Modern C++. C++11之 ...

  4. Effective Modern C++:08调整

    41:针对可复制的形参,在移动成本低且一定会被复制的前提下,考虑将其按值传递 class Widget { public: void addName(const std::string& ne ...

  5. Effective Modern C++:07并发API

    C++11的志伟功勋之一,就是将并发融入了语言和库中,因此在C++的历史上,程序员可以首次跨越所有平台撰写具有标准行为的多线程程序. 35:优先选用基于任务而非基于线程的程序设计 如果需要以异步的方式 ...

  6. Effective Modern C++:06lambda表达式

    lambda表达式实际上是语法糖,任何lambda表达式能做到的,手动都能做到,无非是多打几个字.但是lambda作为一种创建函数对象的手段,实在太过方便,自从有了lambda表达式,使用复杂谓词来调 ...

  7. Effective Modern C++:05右值引用、移动语义和完美转发

    移动语义使得编译器得以使用成本较低的移动操作,来代替成本较高的复制操作:完美转发使得人们可以撰写接收任意实参的函数模板,并将其转发到目标函数,目标函数会接收到与转发函数所接收到的完全相同的实参.右值引 ...

  8. Effective Modern C++翻译(1):序言

    /*********************************************************** 关于书: 书是我从网上找到的effective Modern C++的样章,内 ...

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

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

随机推荐

  1. python 日记 day1

    1.python2 与 python3 的区别:   a. python2 源码不标准,混乱,重复代码太多.默认方式是ascii码,解决方式:#-*- encoding:utf-8 -*-   b. ...

  2. java基础之集合(Set,Map,List)总结

    一.Collection接口 1.Collection接口两个子接口     -List接口         -ArrayList:         -Vector:         -LinkedL ...

  3. 使用C#反射实现用户控件调用父页面方法

    using System.Reflection; MethodInfo mi = this.Page.GetType().GetMethod("GetUserName"); //该 ...

  4. int 13h,磁盘中断

    直接磁盘服务(Direct Disk Service——INT 13H)  00H —磁盘系统复位 01H —读取磁盘系统状态 02H —读扇区 03H —写扇区 04H —检验扇区 05H —格式化 ...

  5. sql 书写顺序

    SELECT select_list [ INTO new_table ] FROM table_source [ WHERE search_condition ] [ GROUP BY group_ ...

  6. Luogu P3960 列队(动态开点线段树)

    P3960 列队 题意 题目描述 Sylvia 是一个热爱学习的女孩子. 前段时间,Sylvia 参加了学校的军训.众所周知,军训的时候需要站方阵. Sylvia所在的方阵中有\(n \times m ...

  7. 树hash

    判断树的同构,采用树hash的方式. 树hash定义在有根树上.判断无根树同构的时候,可以比较重心为根的hash值或者比较每个点为根的hash值. h[x]表示x为根的子树的hash,g[x]表示x为 ...

  8. 微信小程序中自定义swiper轮播图面板指示点的样式

    重置样式: .swiper{ width: 100%; height: 240px; margin-bottom: 0.5rem; position:relative; } div.wx-swiper ...

  9. Excel 表格中根据某一列的值从另一个xls文件的对应sheet中查找包含其中一列的内容(有点拗口)

    =VLOOKUP(C3&"*",INDIRECT("'[2008-2016年三地商务明细表.xls]"&L3&"年北京'!$D ...

  10. 使用tomcat部署多个站点,访问时当然不能带上下文路径咯

    参考 http://blog.sina.com.cn/s/blog_6341fc0f0100lzaj.html tomcat的server.xml文件(比如C:\Program Files\Apach ...