06 Use the explicitly typed initializer idiom when auto deduces undesired types
Item 5 列出了很多使用 auto 的优势。不过,有的时候 auto 的推导的类型并不是我们想要的。比如有一个接受 Widget 返回 std::vector<bool> 的函数,每一个 bool 表示 Widget 是否提供特定的能力。
假定 bit 5 表示传入的 Widget 是否是高优先级的。我们可能会写如下代码:
Widget w;
bool highPriority = features(w)[5]; // is w high priority?
processWidget(w, highPriority); // process w in accord with its priority
上述代码工作的很好,如果将 highPriority 显式声明的类型改为 auto:
代码能够通过编译,但是行为不可预期了。
原因是当使用 auto 之后,highPriority 类型不再是 bool 而是 std::vector<bool>::reference。std::vector::operator[] 返回的类型一般情况是 T&,但是 std::vector<bool> 不是,因为 C++ 的规范使得无法引用一个具体的 bit。std::vector<bool>::reference 的行为很像 bool&,可以转化成 bool(不是 bool&),所以显式地写类型 bool 是没有问题的。
具体行为依赖于实现。std::vector<bool>::reference 往往包含一个指向某个包含该 bit 的字,外加一个偏移量。这是一个临时对象。这个临时对象拷贝给了 highPriority,等语句结束的时候,这个临时对象就销毁了。highPriority 包含了一个悬垂指针。
std::vector<bool>::reference 是代理类(proxy class)的例子。第四章介绍的智能指针也是代理类的例子。
std::shared_ptr 和 std::unique_ptr 对用户可见,而 std::vector<bool>::reference 某种程度上是不可见的,这就导致了问题。
另一个常见的情况是表达式模板。考虑类 Matrix 的四个对象 m1, m2, m3, m4
为了效率,Matrix 的 operator+ 可能返回的类型是 Sum<Matrix, Matrix>,这是 Matrix 的代理类,能够隐式地转化为 Matrix 类型。上面代码右边的类型很可能是 Sum<Sum<Sum<Matrix, Matrix>, Matrix>, Matrix> 然后能够隐式地转化成 Matrix 类型。
看不见的代理类和 auto 配合会有问题。这些类型的对象的生命周期应该局限于一个语句,但是创建这些类型的对象会违反这一点。
那么现在有两个问题,第一个如何识别用到了代理类,第二个是如何修复。
首先看第一个问题。尽管代理类在设计的时候会尽可能不让程序员感知,但使用的类库还是会记录下用法。对类库越熟悉,那么就不会对类库的代理类感到束手无策。
如果没有文档,头文件是一个很好的信息来源。很少有类库会完全对客户端隐藏代理类。函数返回类型是代理类,然后让用户使用,所以可以从函数签名得知代理类的存在。比如 std::vector<bool>::operator[] 的函数签名
namespace std // from C++ Standards
{
template <class Allocator>
class vector<bool, Allocator>
{
public:
class reference
{
};
reference operator[](size_type n);
};
}
假设我们知道 operator[] for std::vector<T> 通常返回类型是 T&,那么这个不遵循惯例的返回类型就是有效提示。关注接口往往会发现代理类的存在。实践中,程序员是在处理诡异的编译错误或者 debug 单元测试时发现代理类的存在。
一旦发现了代理类,且 auto 推导的结果是代理类类型而不是被代理的类型,如何修复呢?解决方法不是简单的不用 auto,因为这不是 auto 的错,而是强行进行不同的类型推导。作者称为显式类型化初始化惯用法(the explicitly typed initializer idiom)。
这种方法仍旧使用 auto 声明变量,不过将类型转化为希望 auto 推导的类型。比如
features(w)[5] 返回 std::vector<bool>::reference 类型的对象,不过 static_cast 把类型转化成了 bool,auto 推断变量 highPriority 的类型也是 bool。运行时,std::vector<bool>::reference 能够转化成 bool 类型,在转化的过程中,其指针有效,也就是指向 std::vector<bool> 内部字段,然后解引用,获取 bool 类型的值。这样就避免了前面说的未定义行为。最后用这个 bool 值来初始化变量 highPriority。
前面 Matrix 的例子可以写作
这种方法还能够强调程序员的意图:故意从初始化表达式的类型转化到一个不同的变量类型。比如一个计算容忍度的函数
返回类型是 double。如果我们程序中觉得 float 的精度足够了,可能会写如下代码
但是这并没有表明我们有意减少精度。如果使用显示类型化初始化惯用法的话,写作
类似的,我们有时将一个浮点数存到一个整型变量中。比如计算一个能够随机访问的容器的下标,我们有一个浮点数,范围是 0 到 1.0,表示距离零号元素的距离,计算结果是浮点数,但是对于下标而言,放到整型变量里面足够了。
这种写法略显晦涩,没有显示出我们的意图,就是想将 double 类型的值存到 int 类型的变量中。下面的写法就好多了。
Things to Remember
- "Invisible" proxy types can cause
autoto deduce the "wrong" type for an initializing expression. - The explicitly typed initializer idiom forces
autoto deduce the type you want it to have.