1.更多的使用auto而不是显式类型声明
-
将大段声明缩减成auto
- 例如:
typename std::iterator_traits<It>::value_type currValue = *b; auto currValue = *b;
- 例如:
-
使用auto可以防止变量未初始化
- 例如:
int x1; //正确,但是未初始化 auto x2; //错误,没有初始化 auto x3 = 3; //正确,声明并初始化
- 例如:
-
在模板函数中可以使用auto来完成变量的自动类型推导
-
例如:
template<typename It> void dwim(It b, It e) { for(; b!=e; ++b) { auto currValue = *b; ... } }
-
-
使用auto来自动推断lambda表达式的返回结果类型
-
例如:
auto derefLess = [](const std::unique_ptr<Widget>& p1, const std::unique_ptr<Widget>& p2){ return *p1 < *p2; };
-
相比之下,使用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; }; -
这两种表达形式的区别是:
-
auto声明的变量,占用和lambda表达式同样大的内存空间
-
使用std::function声明的变量对于任何函数都是固定大小的空间,如果空间不足,就会在堆上申请内存来存储这个闭包。另外,由于限制内联,函数对象不得不产生一次间接函数调用。
-
结果是:std::function对象通常使用更多的内存,执行速度也比auto要慢。
-
-
-
使用auto来避免"type shortcuts"
-
例如:
std::vector<int> v; ... unsigned sz = v.size();// v.size()返回值类型是 std::vector<int>::size_type, 指定是一种无符号整型类型
上述代码在32位windows上,unsigned和std::vector<int>::size_type都是32位,但是在64位windows上,unsigned是32位而std::vector<int>::size_type是64位,因此在不同的机器上运行相同的代码可能会出错,这种与底层系统耦合性较强的错误不应该出现。
- 因此,正确的用法如下:
auto sz = v.size();
-
-
使用auto声明变量来避免类型不匹配时的隐式转换带来的额外代价
-
例如:
std::unordered_map<std::string, int> m; ... for(const std::pair<std::string, int>& p : m) { ... // do somethins with p }
上述代码的毛病在于:std::unordered_map的key部分是const属性,因此哈希表中的std::pair类型实际上应该是std::pair<const std::string, int>。但是上述循环中声明的是一个const std::pair<std::string, int>,因此编译器不得不在这两种类型中做一个转换,首先为了得到一个std::pair<std::string, int>,编译器需要从m中对每个对象进行一次拷贝,创建一系列临时变量,然后再将这些临时变量依次绑定到引用p,在循环结束时,这些临时变量再被编译器进行销毁。
-
正确的做法应该是:
for( const auto& p : m) { ... // as before }
-
-
有关代码可读性的建议:
-
如果使用显示类型声明使得代码更清晰且更容易维护,那么就应该使用显示类型声明,否则就应该试着使用auto
-
通过auto声明的变量,如果想要方便获取是什么类型,可以通过命名规则来间接表示类型。
-
2.当auto推导出错误类型时使用显式类型初始化方式
-
当表达式返回的类型是代理类的类型时,不能使用auto
- 例1:
std::vector<bool> features(const Widget& w);//提取出Widget对象的特征,并以vector<bool>的形式返回,每一个bool代表该feature是否存在 Widget w; ...
//访问特定的feature标志位5 bool highPriority = features(w)[5];//(1) auto highPriority_alt = features(w)[5];//(2) ...
//根据上述标志位的值来处理Widget对象 processWidget(w, highPriority);//(3) processWidget(w, highPriority_alt);//(4) ...上述代码中(1)(3)可以正常运行,但是(2)(4)就会出现未定义行为,这是为什么?
因为std::vector<bool>虽然持有bool,但是operator[]作用于vector<bool>时,并不会返回vector容器中元素的引用([]操作返回容器内元素的引用对于其他类型都适用,唯独bool不适用),而是返回一个std::vector<bool>::reference类型的对象。为什么会存在这种类型的对象呢?因为vector<bool>是通过紧凑的形式来表示bool值,每一个bit代表一个bool。这给[]操作造成了困难,因为对于std::vector<T>,[]操作理应返回的是一个T&对象,但是C++禁止返回对bit的引用,也就是不能返回bool&,那么就得想办法返回一个对象来模拟bool&的行为。因此,std::vector<bool>::reference对象就出现了,它可以在需要的地方自动从bool&转换成bool类型。所以,在(1)中,隐式自动转换是成功的,而在(2)中,auto自动接收了std::vector<bool>::reference对象的类型,没有发生转换,而该对象实际指向的是一个临时std::vector<bool>对象的内部内存地址,当执行完这条语句后,该临时对象被销毁,那么auto保存的地址也就属于悬空地址。在(4)中就会出发未定义行为。 - 代理类介绍
- std::vector<bool>::reference是代理类的一个例子,它们存在的目的是模拟和增强其他类型的行为。例如标准库中智能指针类型也是代理类的例子,它们负责对原始指针指向资源的管理。
- 有一些代理类是对用户可见的,比如std::shared_ptr,std::unique_ptr。而另一些代理类则是用户不可见的,比如: std::vector<bool>::reference和std::bitset::reference。
- C++某些库中使用的叫做表达式模板的技术也属于这个范畴,这种库是为了改善数值计算代码的效率。例2:
Matrix m1, m2, m3, m4; ... Matrix sum = m1 + m2 + m3 + m4;
如果operator+操作返回的是一个代理类比如:Sum<Matrix, Matrix>而不是结果本身也就是Matrix对象,那么这样就可以高效计算这个表达式。
因为对象的类型会被编码到整个初始化表达式,比如:Sum<Sum<Sum<Matrix, Matrix>, Matrix>, Matrix>。
- 一般性规则,不可见的代理类不适用与auto,因为代理类对象一般只存活于一条语句内,因此创建代理类对象的变量违反了基本库设计假设。
- 例1:
-
auto推到出代理类类型时,需要对表达式做代理类类型到实际类型的静态转换,而不是弃用auto
- 针对上面的例1:
auto highPriority = static_cast<bool>(features(w)[5]);
-
针对上面的例2:
auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);
- 针对上面的例1:
3.总结
auto自动类型推导可以精简代码,避免隐式转换带来开销,同时增强程序可移植性和减少重构复杂性;但也由于与隐式代理类的冲突,造成了一些潜在问题,但是这些问题不是auto引起的,而是代理类本身的问题,因此显式静态类型转换可以保留auto的优点,同时保证程序的正确性。