Effective Modern C++ Item 6

Use the explicitly typed initializer idiom when auto deduces undesired types

尽管 auto 相对于显式声明类型好处很多, 但还是有一些棘手的陷阱需要注意。

假如我有一个函数,参数为Widget,返回一个std::vector<bool>,这里的bool表示Widget是否提供一个独有的特性。我们可以写这样的代码:

1
2
3
4
std::vector<bool> features(const Widget& w);
Widget w;
bool highPriority = features(w)[5]; //w高优先级吗?
processWidget(w, highPriority); //根据它的优先级处理w

以上代码没有问题。但是使用auto代替highPriority的显式指定类型,就会出错:

1
2
auto highPriority = features(w)[5];     //w高优先级吗?
processWidget(w,highPriority); //未定义行为!

为什么是一个未定义行为呢?std::vector<bool>以紧凑的形式存储它的bool,每个bool占一个bit。std::vector<T>operator[]理应返回一个T&,但是C++禁止对bits的引用。因此,有一个返回的代理对象(proxy)来模仿 bool 的行为。std::vectoroperator[]的返回类型其实是std::vector::reference

在第一个版本中,features返回的std::vector<bool>::reference对象被隐式转换赋值给bool变量highPriority

而在第二个版本中,auto推导highPriority的类型为std::vector<bool>::reference,包含一个指向这些bits的指针。highPriority是这个std::vector<bool>::reference的拷贝,所以也包含一个指针。在这个语句结束的时候std::vector<bool>::reference将会被销毁,因此作为临时变量的highPriority包含一个悬置的指针,会造成未定义行为。

事实上, “代理(Proxy)”的设计模式是软件设计模式中历史最悠久的成员之一。一些代理类被设计于用以对客户可见。比如std::shared_ptrstd::unique_ptr。其他的代理类则或多或少不可见,比如std::vector<bool>::reference

不可见代理类里一些C++库用了表达式模板的黑科技,能够提高数值运算的效率。举个例子:

1
Matrix sum = m1 + m2 + m3 + m4;

对两个Matrix对象使用operator+将会返回如Sum<Matrix, Matrix>这样的代理类而不是直接返回一个Matrix对象。上述表达式的结果可能是 Sum<Sum<Sum<Matrix, Matrix>, Matrix>, Matrix>, 如果需要具体求值时再一次性将其转换为 Matrix 对象,节省了计算次数。

不可见的代理类通常不适用于auto,这样类型的对象的生命期通常不能活过一条语句。 更好的解决方案是使用显式类型初始器惯用法:使用auto声明一个变量,然后对表达式强制类型转换(cast)得出期望的推导结果。

1
2
3
4
auto highPriority = static_cast<bool>(features(w)[5]);
auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);
double calcEpsilon();
auto ep = static_cast<float>(calcEpsilon());

重点

  • 不可见的代理类(proxy)可能会导致 auto 从表达式推断出“错误”的类型。
  • 显式类型初始器惯用法强制auto推导出你想要的结果。