Item 5解释了比起显式指定类型,使用 auto 来声明变量提供了大量技术上的优点,但是有时候 auto 的类型推导出zigs(这个类型),但是你想要的是zag(另外一个类型)。举个例子,假设我有一个函数以 Widget 为参数并且返回一个 std::vector
std::vector<bool> features(const Widget& w);
进一步假设 bit 5 指示了 Widget 有高优先级。因此我们写了如下的代码:
Widget w; ... bool highPriority = features(w)[5]; ... processWidget(w, highPriority); //根据w的优先级处理w
这段代码没有错。它工作得很好。但是如果我们做一些看起来无害的操作,也就是用auto来替换 highPriority 的显式类型声明,
auto highPriority = features(w)[5];
情况改变了。所有的代码将能继续通过编译,但是它的行为不再和预测的一样了:
processWidget(w, highPriority); //未定义的行为!
就像注释指示的那样,调用 processWidget 现在是未定义的行为。但是为什么会这样?回答有可能很奇怪。在使用 auto 的代码中, highPriority 的类型不再是 bool 、尽管 std::vector
std::vector
记住了这些信息后,再看看源代码的这一部分:
bool highPriority = features(w)[5]; //显式声明 //highPriority的类型
这里, features 返回一个 std::vector
对比对 highPriority 使用 auto 初始化声明时发生的情况:
auto highPriority = features(w)[5]; //推导highPriority的类型
再一次, features 返回一个 std::vector
它拥有的值依赖于 std::vector
对于 features 的调用返回一个 std::vector
processWidget(w, highPriority); //未定义行为! //highPriority持有悬挂的指针
std::vector
对于客户来说,一些代理类被设计成可见的。举个例子,这就是 std::shared_ptr 和 std::unique_ptr 的情况。另外一种代理类被设计成或多或少不可见的。 std::vector
同样在这个阵营中,一些C++库中的类使用一种叫expression template的熟知科技。这样的库最初开发出来是为了提升算术运算代码的效率。给出一个类 Matrix 和 Matrix 对象 m1 , m2 , m3 和 m4 ,举个例子,表达式
Matrix sum = m1 + m2 + m3 + m4;
能被计算得更加有效率一些,只需要让 Matrix 对象的 operator + 返回一个结果的代理来代替结果本身。也就是,两个 Matrix 对象的 operator + 将返回一个对象,这个对象用像 Sum
一般情况下,“看不见的”代理类不能和 auto 很好地结合在一起。这样的对象的生命周期常常被设计成不能超过一条简单语句,所以创造这些类型的变量将违反基础库设计时假设的情况。这就是 std::vector
你因此想要避免这样形式的代码:
auto someVar = expression of "invisible" proxy class type //“不可见的”代理类的表达式
但是你要怎么意识到你在使用代理对象呢?制造代码的人不太可能主动解释它(代理类)的存在。至少在概念上,它们被假设为“不可见的”。并且一旦你找到了它们,你就真的能抛弃 auto 以及在 item 5 中说明的 auto 的众多优点吗?
让我先解决“如何去找到它们”的问题。尽管“不可见的”代理类被设计成,在日常使用中,程序员不会发现它们,但库生产者在使用它们时常常会用文档标示出他这么做了。你越多地让你自己熟悉你所使用的库的基础设计决策,你就越少地被库中的代理类给偷袭。
当文档出现遗漏的时候,头文件会填补这个缺陷。想要完全隐藏掉代理类对源代码来说是几乎不可能的。典型地,它们就是用户想要调用的函数的返回,所以函数签名常常反映了它们的存在。这里有 std::vector
namespace std{ template <class Allocator> class vector<bool, Allocator>{ public: ... class reference {...}; reference operator[](size_type n); ... }; }
假设你知道一个 std::vector
作为实践,很多开发者只有在当他们尝试追踪神秘的编译错误,或者调试出不正确的单元测试结果时,才能发现代理类的存在。不管你怎么发现他们,一旦 auto 推导出一个代理类类型替换了被代理的类型,解决方法不是禁止 auto 的使用。 auto 它本身没有问题。问题是 auto 不能推导出你想要它推导的类型。解决方法是强制一个不同的类型推导。你可以使用被我成为“显式类型初始化语法”的方法。
显示类型初始化语法涉及用 auto 声明一个变量,但是需要把初始化表达式的类型转换到你想要 auto 推导的类型。举个例子,这里给出怎么用这个方法来强制 highPriority 转换到 bool :
auto highPriority = static_cast<bool>(features(w)[5]);
这里, features(w)[5] 继续返回一个 std::vector
对于 Matrix 的例子,显式类型初始化语法看起来将会是这样:
auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);
这个语法的应用不止限制在初始化时产生的代理类类型上。它在当你故意创建一个变量,让它的类型不同于初始化表达式产生的类型时同样可以起到一个强调的作用。举个例子,假设你有一个函数计算一些公差值:
double calcEpsilon(); //返回公差值
calcEpsilon明确返回一个 double ,但是假设你知道,对于你的应用, float 的精度就足够了,并且你更关心 float 和 double 的大小。你可以声明一个 float 类型的变量来存放 calcEpsilon 的返回值,
float ep = calcEpsilon(); //double -> float的隐式转换
但是这几乎没有宣布“我故意降低了函数返回值的精度”。这时就可以用显式类型转换语法来声明它,动起来:
auto ep = static_cast<float>(calcEpsilon());
同样的理由可以应用在你想故意用整形来存放浮点型的表达式。假设你需要用随机存取迭代器(比如, std::vector , std::deque 或 std::array 的迭代器)计算容器元素的下标时,并且你给出了一个从0.0到1.0的 double 值来标示从容器的开始到你需要的元素的位置有多远。(0.5将标示着容器的中间位置)进一步假设你确信下标计算的结果适合作为一个 int 。如果容器是 c ,并且 double 值是 d ,你可以这样计算下标:
int index = d * c.size();
但是这模糊了一个事实,就是你故意地把右边的 double 转换成了一个 int 。显式类型初始化语法让事情变得易懂了:
auto index = static_cast<int>(d * c.size());
你要记住的事