Effective Modern C++ 读书笔记:auto
条款 5:优先选用 auto
使用 auto 会带来很多好处,比如:
- 不需要关注一些复杂的类型声明,比如模板函数和 lambda 表达式类型
- 不会忘记初始化变量(
auto要求必须初始化) - 一些不希望发生的隐式类型转换
- 代码类型变化时,不需要在多处修改类型,
auto自适应
使用 auto 来取代显式指定类型的建议,一个很大的反对声音,就是无法从代码中直观地看出一个变量的类型。针对这种强有力地反对声浪,实际上有多种理由来反驳,比如说:
- 大多数现代编程语言,基本都支持类型推导的特性
- 一个好的名字,比一个类型要重要的多。比如我们不需要知道迭代器的类型,但只要从名字中看出这是一个迭代器,就可以
- 一些现代化的编辑器,都支持显示类型,这包括 vim/emacs 这些有年代感的软件
- 默认使用
auto,但在一些特殊情况,比如需要明确指定类型时,显式声明类型
有关于 auto 的讨论,网络上有很多,我这里不一一列举。下面看几个书中的例子。
案例 1:使用 auto 替代函数对象
以下代码,我们希望使用一个类型来接受 lambda 表达式,通常的做法是使用 auto,让编译器去推导实际的类型,但另外,也可以通过一个 std::function 来接受 lambda 表达式。注意,lambda 表达式的类型并不是 std::function,它只有编译器知道。因为 std::function 可以接受任何可调用对象,所以也就可以接受 lambda 表达式。
1 | // 最常见的写法,使用 auto 类型 |
使用 std::function 的版本,首先看着非常啰嗦。除此之外,还有一些背后的差异。
第一点是,std::function 比 auto 的版本,更占用内存。auto 在类型推导后,和它的实际类型的占用空间是一致的,而 std::function 是一个模板函数,它的大小是固定的(不依实际类型而变化),除了其本身,还包含固定大小的闭包变量。如果固定的内存并不够用,编译器就会生成在堆上分配额外空间来存储闭包变量,也就导致了更多的内存开销。
第二点是,std::function 通常不会内联,所以结合第一点,也就会更慢。
案例 2:遗漏类型修饰导致性能开销
以下代码中,我们在一个循环中对 unordered_map 的容器做遍历:
1 | std::unordered_map<std::string, int> m; |
实际上,变量 m 的类型写错了,因为字典的键值应该是 const,所以其类型应该是 std::unordered_map<const std::string, int>。
这种错误编译器可以处理,它会在遍历键值对时,主动一个个将键值类型转换为 const std::string,实现方法是重新拷贝一份,再将拷贝后的 const 版本和 p 绑定在一起。
这就带来了性能开销。
使用 auto 既安全又简洁地解决了这个问题:
1 | std::unordered_map<std::string, int> m; |
条款 6:当 auto 推导不符合时,使用显式类型的初始化
大多数情况下,auto 推导出来的类型,和实际我们希望它应该是的类型是一致的。
但有一种情况是例外。讨论以下示例代码:
1 | // 这是一个函数声明,返回 vector<bool> |
这个代码没有任何问题,但如果我们将使用 features 的代码改为:
1 | auto res = features(w)[5]; |
问题就出现了。
表面原因可能很容易理解,如果了解过一些 std::vector 的知识,就会知道,获取 std::vector<bool> 的元素,并不会直接返回 bool。C++ STL 中,std::vector<bool> 的 operator[] 操作,返回的是一个特别的类型,std::vector<bool>::reference。也就是说,auto 类型推导的结果并不是 bool,而是 std::vector<bool>::reference 类型。
这是因为,通常 operator[] 操作返回的应该是某个元素的引用,而 C++ 中不允许对位进行引用,所以设计了这样一种结构。类似的问题也存在于 std::bitset 中。
由于 std::vector<bool>::operator[] 返回的是 std::vector<bool>::reference 类型,而后者的实现,有可能是一个指向 word 字内存的引用和一个字中位的偏移(std::vector<bool> 中使用 word 字来保存每个 bit)。
另外,我们注意到,features(w) 返回的实际是一个临时对象,那么,当临时对象被销毁时,std::vector<bool>::reference 中的内存引用就会失效,这个失效即发生在 auto res 被初始化后,从而导致下一行代码出现悬挂引用。
如果不使用 auto,使用 bool 指定实际类型,就不会有这个问题,原因是 std::vector<bool>::reference 实现了向 bool 的隐式类型转换。std::vector<bool>::reference 这种结构被称为代理类,在 C++ 实践中其实广泛存在,最常见的就是 std::shared_ptr<T>和 std::unique_ptr<T>。如果使用 auto 来推导返回代理类类型的对象类型,便会发生上述问题。
既想使用 auto 还想解决这个问题的办法,只能是使用显式的类型转换:
1 | auto res = std::static_cast<bool>(features(w)[5]); |
这种写法相比于最开始 bool res = features(w)[5] 的写法,是更值得推荐的,原因是,它可以显式表达出来这种类型转换,不容易引起错误。事实上,static_cast 总是应该被推荐的,任何隐式类型转换,都可能引入潜在的问题,而迷惑程序员。
这可能是唯一一个会阻碍你使用 auto 的理由,我认为这也是 C++ 设计的一个遗憾,能做的就是,在编码时,使用 auto 但不要忽视类型,C++ 依然是一个强类型语言。另外,对这种常见的代理类要有足够的敏感度,比如获取 std::vector<bool> 的元素,获取 std::bitset 的位,获取智能指针的原始裸指针。
陷阱:auto 固然好用,但不是万能的,使用 auto 时,时刻还需要留意背后推导的类型是什么。
这一章比较短,也没有什么难以理解的内容,更多的是一种编程习惯。
本系列其他文章:
- Effective Modern C++ 读书笔记:类型推导
- Effective Modern C++ 读书笔记:auto
- Effective Modern C++ 读书笔记:转向现代C++
- Effective Modern C++ 读书笔记:智能指针
- Effective Modern C++:右值引用、移动语义和完美转发
- Effective Modern C++:lambda 表达式
- Effective Modern C++:并发 API
- Effective Modern C++:微调
本文同步发布在知乎账号下:https://zhuanlan.zhihu.com/p/1932162651242340425













