02 auto

优质
小牛编辑
122浏览
2023-12-01

05 用 auto 替代显式类型声明

  • auto 声明的变量必须初始化,因此使用 auto 可以避免忘记初始化的问题
int a; // 潜在的未初始化风险
auto b; // 错误:必须初始化
  • 对于名称非常长的类型,如迭代器相关的类型,用 auto 声明可以大大简化工作
template<typename It>
void f(It b, It e)
{
  while (b != e)
  {
    auto currentValue = *b;
    // typename std::iterator_traits<It>::value_type currentValue = *b;
    ...
  }
}
  • lambda 生成的闭包类型是编译期内部的匿名类型,无法得知,使用 auto 推断就没有这个问题
auto f = [](auto& x, auto& y) { return x < y; };
// std::function 的模板参数中不能使用 auto
std::function<bool(int&, int&)> f = [](auto& x, auto& y) { return x < y; };
  • 除了明显的语法冗长和不能利用 auto 参数的缺点,std::function 与 auto 的最大区别在于,auto 和闭包类型一致,内存量和闭包相同,而 std::function 是类模板,它的实例有一个固定大小,这个大小不一定能容纳闭包,于是会分配堆上的内存以存储闭包,导致比 auto 变量占用更多内存。此外,编译器一般会限制内联,std::function 调用闭包会比 auto 慢
  • auto 可以避免简写类型存在的潜在问题。比如如下代码有潜在隐患
std::vector<int> v;
unsigned sz = v.size(); // v.size() 类型实际为 std::vector<int>::size_type
// 在 32 位机器上 std::vector<int>::size_type 与 unsigned 尺寸相同
// 但在 64 位机器上,std::vector<int>::size_type 是 64 位,而 unsigned 是 32 位
  • 如下代码也有潜在问题
std::unordered_map<std::string, int> m; // m 的元素类型实际是 std::pair<const std::string, int>
for (const std::pair<std::string, int>& p : m) ... // 类型不一致,仍要转换,期间要构造大量临时对象
  • 如果显式类型声明能让代码更清晰或有其他好处就不用强行 auto,此外 IDE 的类型提示也能缓解不能直接看出对象类型的问题

06 auto 推断出非预期类型时,先强制转换出预期类型

  • auto 推断得到的类型可能与直觉认知不同
std::vector<bool> f()
{
  return std::vector<bool>{ true, false };
}

bool a = f()[0];
auto b = f()[0];

if (a) {} // OK
if (b) {} // 错误:未定义行为,b 类型是 std::vector<bool>::reference
  • std::vector\ 对于 bool 类型的特化,为了节省空间,每个元素用一个 bit(而非一个 bool)表示,于是 operator[] 返回的应该是单个 bit 的引用,但 C++ 中不存在指向单个 bit 的指针,因此也不能获取单个 bit 的引用
std::vector<bool> v { true, false };
bool* p = &v[0]; // 错误
std::vector<bool>::reference* q = &v[0]; // 正确
  • 因此需要一个行为类似单个 bit 并可以被引用的对象,也就是 std::vector\::reference,它可以隐式转换为 bool
bool x = f()[0];
  • 而 auto 推断不会进行隐式转换
auto x = f()[0]; // std::vector<bool>::reference x = f()[0];
// x 不一定指向 std::vector<bool>的第 0 个 bit,这取决于 std::vector<bool>::reference 的实现
// 一种实现是含有一个指向一个 machine word的指针,word 持有被引用的 bit 和这个 bit 相对 word 的 offset
// 于是 x 持有一个由 opeartor[] 返回的临时的 machine word 的指针和 bit 的 offset
// 这条语句结束后临时对象被析构,于是 x 含有一个空悬指针,导致后续的未定义行为
if (x) {} // 相当于int* p; if(p) {}
  • std::vector\::reference 是一个代理类(proxy class,模拟或扩展其他类型的类)的例子,比如 std::shared_ptrstd::unique_ptr 是很明显的代理类。还有一些为了提高数值计算效率而使用表达式模板技术开发的类,比如给定一个 Matrix 类和它的对象
Matrix sum = m1 + m2 + m3 + m4;
  • Matrix 对象的 operator+ 返回的是结果的代理而非结果本身,这样可以使得表达式的计算更为高效
auto x = m1 + m2; // x可能是 Sum<Matrix, Matrix> 而不是 Matrix 对象
  • auto 推断出代理类的问题实际很容易解决,事先做一次到预期类型的强制转换即可
auto x = static_cast<bool>(f()[0]);