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 = 0; // 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)[5]; // is w high priority? processWidget(w, highPriority); // process w in accord with its priority
这份代码没有任何问题。但是如果我们改用auto:
auto highPriority = features(w)[5]; // 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)[5];
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)[5]);
这里, 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());