用auto代替显式类型声明
在 C++11 之前,程序员需要手写复杂的迭代器、指针等类型,但是在 C++11
引入 auto 自动类型推导(automatic type
deduction)后,这些复杂繁复的工作全部交给编译器处理。
虽然 auto 可以解放双手,但 auto
的核心价值在于提升代码的正确性、性能和可维护性。
auto 的优点
解决变量未初始化的问题
在使用显式类型声明时,经常会忘记初始化变量,这就导致变量存储着不确定的值,可能会引发程序错误。
但是 auto
类型声明必须提供初始值,在编译期就强制避免了变量未初始化的问题。
1 | |
避免冗长且不直观的类型
在 C++11 中,lambda
匿名函数的类型是由编译器在编译期独一无二生成的,无法由程序员写出,但是利用
auto 就可以声明其真实类型的变量。例如:
1 | |
在 C++14 中,lambda 表达式的参数也可以使用 auto
类型,使其可以接收更广泛的类型:
1 | |
或许你会想到使用 std::function
类型的对象来接收一个闭包(closure),比如:
1 | |
但是,使用 auto 远远好于
std::function。除了不用拼写冗长的类型外,auto
推导得到的是闭包的真实类型,而 std::function
是一个实例化的模板,这就导致 std::function
的大小固定,当闭包的大小超过规定容量后,std::function
还会在堆区额外开辟内存;并且由于内部实现,std::function
的调用更慢。
避免”类型不匹配”导致的性能损耗与隐式转换陷阱
auto 可以避免一些隐式转换陷阱。比如:
std::vector<int>::size() 的返回值类型是
std::vector<int>::size_type(通常是
size_t),该类型存储的是无符号整数。在不同字长的机器上,size_type
的存储容量也不同;如果将其赋值给固定大小的 unsigned
类型,在 64 位机器上可能会出现截断的情况。使用 auto
接收返回值则始终得到正确的类型。
auto 还可以避免一些性能损耗。比如:
对于 std::map<std::string, int> m,你可能会用
for (const std::pair<std::string, int>& p : m)
来遍历其中的元素。但是 std::map 的键实际上是
const 修饰的,即
std::pair<const std::string, int>。当你用
std::pair<std::string, int> 类型去遍历时,由于
const std::pair<const std::string, int>& 绑定到
const std::pair<std::string, int>&
需要类型转换,编译器会为每一个元素创建一个临时副本,产生极大的拷贝开销;同时由于循环内部操作的是副本,m
内的真实值并不会被改动。
因此更好的写法是用 auto
类型进行遍历:for (const auto& p : m)。
auto 类型推导的陷阱
使用
auto,除了会使语义不清晰外,有时也会出现一些与预期类型不匹配的情况。比如:
1 | |
features() 会返回一个临时的
std::vector<bool> 对象,我们通过 []
获取其内部元素的引用,分别用 bool 和 auto
类型的变量来接收,但使用 auto
类型的变量却会导致未定义行为。
这是因为 bool 类型为 1 bit 大小,但是 C++
只能以字节为单位进行寻址,无法直接返回单个 bool
的引用,于是标准库引入了 std::vector<bool>::reference
这个代理类(proxy class)。
std::vector<bool>::reference 内部存储了一个指向
std::vector<bool> 的指针和 bool
值在其内部的偏移量。当我们用 bool
接收时会发生隐式转换,bool 变量会通过值传递得到容器内的真实
bool 值,因此使用该 bool
变量不会出现问题;但是如果用 auto
去接收则不会发生隐式转换,auto 会被正确推导为
std::vector<bool>::reference
类型。在使用该变量时会根据其内部的指针进行寻址,但由于
features()
返回的临时对象已经析构,此时会因为悬空指针(dangling
pointer)而崩溃。
C++ 中像 std::vector<bool>::reference
这样的类被称为代理类,代理类是设计模式中”代理模式”的体现,在 C++
库设计中很常见,用来模拟或增强某种类型的行为。例如:
- 智能指针(
std::shared_ptr/std::unique_ptr)也是一种代理类,但它们是显式设计给用户用的。 - 隐式的代理类:比如为了提高表达式计算效率的表达式模板(expression templates),如 Eigen 等高性能矩阵库。
auto
在处理代理类时,推导类型往往不符合我们的预期。比如:
在矩阵库中,Matrix sum = m1 + m2 + m3 + m4; 其中
m1 + m2 返回的可能不是一个巨大的 Matrix
对象,而是一个代理类
MatrixSum<Matrix, Matrix>。如果你写
auto sum = m1 + m2 + m3 + m4; 可能会得到
Sum<Sum<Sum<Matrix, Matrix>, Matrix>, Matrix>
类型的变量。
解决方法是使用显式强制类型转换:auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);
这次的插图来自画师 ばなこ武丸
图片地址:https://www.pixiv.net/artworks/147994165
文章内容参考:Effective Modern C++ 42 Specific Ways to Improve Your Use of C++11 and C++14 (Scott Meyers) Item 5-Item 6