【Effective Modern C++】第2章 auto
auto好处:少打一些字,阻止那些由于手动指定型别带来的潜在错误和性能问题
条款5:优先选用auto,而非显示型别声明
-
使用auto必须初始化,就可以避免一系列由未初始化带来的问题
-
auto用在lambda表达式的形参类型声明中,并且可以声明变量直接持有闭包,如:
auto derefLess = //C++14版本 [](const auto& p1, //被任何像指针一样的东西 const auto& p2) //指向的值的比较函数 { return *p1 < *p2; };
-
你可能会想我们完全不需要使用
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::function
方法比auto
方法要更耗空间且更慢,还可能有out-of-memory异常。 -
auto还可以避免一类“型别捷径”的问题。
v.size()
的标准返回类型是std::vector<int>::size_type
,但是只有少数开发者意识到这点。std::vector<int>::size_type
实际上被指定为无符号整型,所以很多人都认为用unsigned
就足够了,举个例子,在Windows 32-bit上std::vector<int>::size_type
和unsigned
是一样的大小,但是在Windows 64-bit上std::vector<int>::size_type
是64位,unsigned
是32位。这意味着这段代码在Windows 32-bit上正常工作,但是当把应用程序移植到Windows 64-bit上时就可能会出现一些问题。谁愿意花时间处理这些细枝末节的问题呢?所以使用auto
可以确保你不需要浪费时间:auto sz =v.size(); //sz的类型是std::vector<int>::size_type
再比如,考虑下面的代码,看起来好像很合情合理的表达,但是这里有一个问题,你看到了吗?
std::unordered_map<std::string, int> m; … for(const std::pair<std::string, int>& p : m) { … //用p做一些事 }
要想看到错误你就得知道
std::unordered_map
的key是const
的,所以hash table(std::unordered_map
本质上的东西)中的std::pair
的类型不是std::pair<std::string, int>
,而是std::pair<const std::string, int>
。但那不是在循环中的变量p
声明的类型。编译器会努力的找到一种方法把std::pair<const std::string, int>
(即hash table中的东西)转换为std::pair<std::string, int>
(p
的声明类型)。它会成功的,因为它会通过拷贝m
中的对象创建一个临时对象,这个临时对象的类型是p
想绑定到的对象的类型,即m
中元素的类型,然后把p
的引用绑定到这个临时对象上。在每个循环迭代结束时,临时对象将会销毁,如果你写了这样的一个循环,你可能会对它的一些行为感到非常惊讶,因为你确信你只是让成为p
指向m
中各个元素的引用而已。使用
auto
可以避免这些很难被意识到的类型不匹配的错误:for(const auto& p : m) { … //如之前一样 }
这样无疑更具效率,且更容易书写。而且,这个代码有一个非常吸引人的特性,如果你获取
p
的地址,你确实会得到一个指向m
中元素的指针。在没有auto
的版本中p
会指向一个临时变量,这个临时变量在每次迭代完成时会被销毁。 -
关于使用auto代替传统类型声明对源码可读性的影响的问题,放松,
auto
是可选项,不是命令,在某些情况下如果你的专业判断告诉你使用显式类型声明比auto
要更清晰更易维护,那你就不必再坚持使用auto
。 -
事实是显式指定类型通常只会引入一些微妙的错误,无论是在正确性还是效率方面。而且,如果初始化表达式的类型改变,则
auto
推导出的类型也会改变,这意味着使用auto
可以帮助我们完成一些重构工作。
要点速记
auto
变量必须初始化,通常它可以避免一些移植性和效率性的问题,也使得重构更方便,还能让你少打几个字。- auto型别的变量都有着条款2和条款6中所述的毛病。
条款6:当auto推导的型别不符合要求时,使用带显式型别的初始化物习惯用法
-
std::vector<bool> features(const Widget& w); //bool highPriority = features(w)[5]; auto highPriority = features(w)[5]; // !!!,highPriority的型别不再是bool了 processWidget(w,highPriority); //未定义行为!
虽然从概念上来说
std::vector<bool>
意味着存放bool
,但是std::vector<bool>
的operator[]
不会返回容器中元素的引用(这就是std::vector::operator[]
可返回除了bool
以外的任何类型),取而代之它返回一个std::vector<bool>::reference
的对象(一个嵌套于std::vector<bool>
中的类)。std::vector<bool>::reference
之所以存在是因为std::vector<bool>
规定了使用一个打包形式(packed form)表示它的bool
,每个bool
占一个bit。那给std::vector
的operator[]
带来了问题,因为std::vector<T>
的operator[]
应当返回一个T&
,但是C++禁止对bit
s的引用。无法返回一个
bool&
,std::vector<bool>
的operator[]
返回一个行为类似于bool&
的对象。要想成功扮演这个角色,bool&
适用的上下文std::vector<bool>::reference
也必须一样能适用。在std::vector<bool>::reference
的特性中,使这个原则可行的特性是一个可以向bool
的隐式转化。(不是bool&
,是**bool
**。) -
std::vector<bool>::reference
是一个代理类(proxy class)的例子:所谓代理类就是以模仿和增强一些类型的行为为目的而存在的类。一些代理类被设计于用以对客户可见。比如std::shared_ptr
和std::unique_ptr
。其他的代理类则或多或少不可见,比如std::vector<bool>::reference
就是不可见代理类的一个例子,还有它在std::bitset
的胞弟std::bitset::reference
。
一个普遍的规律是,“隐形”代理类和auto无法和平共处。这种类的对象往往会设计成仅仅维持到单个语句之内存在。所以以下的代码可能会导致未定义的i行为,要防止写出这样的代码:auto someVar="隐形"代理型别表达式
那么如何发现使用了代理类呢,一是查看接口文档,二是查看头文件的函数签名。这个问题并不是放弃使用auto的理由,解决方案应该是进行另一次型别转换,如使用
static_cast
就可以表明“我故意转为了某种类型”。auto index = static_cast<int>(d * size());
要点速记
- “隐形”的代理类可能会使
auto
从表达式中推导出“错误的”类型 - 显式类型初始器惯用法强制
auto
推导出你想要的结果
参考:Effective Modern C++(中文版)和这里。