条款6:当auto推导出意外的类型时,使用显式的类型初始化语义
条款5解释了使用auto来声明变量比使用精确的类型声明多了了很多的技术优势,但有的时候,当你想要zag的时候,auto可能会推导出了zig。例如,我有一个函数,它以const Widget&作为参数,并且返回std::vector<bool>,每一个bool暗示了Widget是否提供了一个特殊的特性。
std::vector<bool> features(const Widget& w);
进一步假设第5个bool暗示了Widget是否拥有比较高的优先级,我们可以写下这样的代码。
Widget w;
…
bool highPriority = features(w)[5]; // w是否有较高的优先级?
…
processWidget(w, highPriority); //根据w是否拥有较高的
//的优先级来对它进行处理
这段代码没有任何问题,它会很好的工作,但是如果我们声明highPriority时用看起来无害的auto代替精确的类型声明
auto highPriority = features(w)[5]; // w是否有较高的优先级?
在这种情况下,所有的代码都会编译成功,但是它的行为却是未定义的:
processWidget(w, highPriority); //未定义的行为!
就像注释指出的那样,对processWidget的调用行为现在是未定义的了,但是为什么呢,答案可能会十分令人惊讶,在使用auto的代码中,highPriority的类型不再是bool,尽管std::vector<bool>概念上应该持有bool对象,但[]运算符并不返回容器内元素的引用(std::vector::operator[]返回容器的每一个类型除了bool),相反它返回一个std::vector<bool>:reference类型的对象(std::vector<bool>中的内部类)
std::vector<bool>::reference的存在是因为std::vector<bool>内部用一种紧缩的形式来表示bool对象,每一个bit代表一个bool对象,这使得std::vector<bool>的[]运算符出现了问题,因为std::vector<T>的[]运算符应该返回T&类型的对象,但是C++禁止返回对位对象的引用。无法返回bool&,std::vector<bool>的[]运算符返回了一个对象,它的行为看起来很像bool&,为了让这个想法能够成功,std::vector<bool>::reference对象必须能够在bool&都够使用的地方同样适用,在features中,std::vector<bool>::reference实现这个工作是通过一个到bool的隐式转换(不是bool&到bool,为了完整的解释std::vector<bool>::reference模拟bool&行为中使用的技术将会将我们带的太远太远,所以我简单的说这个隐私的转换只是很小的一部分(I’ll simply remark that this implicit conversion is only one stone in a larger mosaic)
带着这个思想,我们再来看一看最初的代码
bool highPriority = features(w)[5]; //精确声明highPriority
//的类型
这里,features返回了一个std::vector<bool>对象,并在这个对象上调用了[]运算符,[]运算符返回了一个std::vector<bool>::reference对象,这个对象为了初始化highPriority对象被隐式的转化为了一个bool对象。highPriority因此最终通过features获得了std::vector<bool>中第5个bit的值,就像它本应该的那样。
对比一下如果用auto声明highPriority会发生什么呢?
features返回了一个std::vector<bool>对象,[]运算符作用在了上面,[]继续返回一个std::vector<bool>::reference对象,但是现在有了一点改变,因为使用了auto来声明highPriority的类型,highPriority并不拥有features返回的std::vector<bool>对象的第5个bit的值。
highPriority的值取决于std::vector<bool>::reference是如何实现的,一种实现方式是std::vector<bool>::reference包含一个指针指向机器字,加上对引用位的偏移,我们考虑一下如果std::vector<bool>::reference是这样实现的,highPriority的初始化意味着什么。
对features的调用返回了一个临时的std::vector<bool>对象,这个对象没有名字,但是为了方便讨论,我这里叫它temp,[]运算符在temp上调用,返回的std::vector<bool>::reference包含了一个指针,指针所指向是数据结构中包含了一个temp内的机器字和相应的偏移量5,highPriority是std::vector<bool>::reference对象的拷贝,所以highPriority也包含一个指向temp中机器字的指针,加上相应的偏移量5,在语句的最后,temp被销毁了,因为这是一个临时对象,因此highPriority包含了一个悬垂指针,导致对processWidget的调用是未定义的。
processWidget(w, highPriority); // 未定义的行为!
// highPriority包含了
// 悬垂指针!
std::vector<bool>::reference是一个代理类的例子,一个类存在的目的是模拟和增强另一些类型的行为,代理类被应用于各种各样的目的,std::vector<bool>::reference的存在是提供一个std::vector<bool>::reference的[]运算符返回了一个对位的引用的错觉,标准库的智能指针类型(参见第4章)移植了裸指针的资源管理(the Standard Library’s smart pointer types (see Chapter 4) are proxy classes that graft resource management onto raw pointers. The)。代理类的特性已经被广泛的建立了,事实上在设计模式的宫殿中代理模式是存在时间最长的成员之一。
一些代理类对客户来说是很显然的,例如std::shared_ptr和std::unique_ptr,而另一些代理类被设计的或多或少有些不可见,例如std::vector<bool>::reference,std::bitset::reference。
同样C++中一些库库中的类使用了一种叫表达式模板的东西,这些库早先的目的是为了提高数字运算(numeric code)的效率,假定有一个Matrix类和4个Matrix对象,m1,m2,m3,m4。
Matrix sum=m1+m2+m3+m4
如果+运算符返回一个结果的代理而不是结果本身的话,运算会更有效率。两个Matrix对象的+可以返回一个代理类,例如Sum<Matrix,Matrix>而不是Matri对象本身。和std::vector<bool>::reference和bool的例子一样,代理类和Matrix之间会有一个隐私的转化,允许代理对象初始化等号右边的sum对象(初始化对象的表达式可能会是Sum<Sum<Sum<Matrix,Matrix>,Matrix>,Matrix>,这个类型肯定需要对客户隐藏起来)
照例,不可见的代理类和auto间相处的并不是很好,这些代理类通常被设计为不会存活超过一条语句,所以创建这样类型的变量违背了基础库的设计假设,就像std::vector<bool>::reference,我们可以看到违背这样假设会引发未定义的行为。
因此你会想要避免这样形式的代码:
auto someVar = expression of "invisible" proxy class type;
但是你应该如何识别代理类呢,使用到代理类的代码不太可能会突显出他们的存在,他们至少在概念上是不可见的,一旦你发现他们,难道你应该抛弃auto和条款5提到的auto带来的大量优点吗?
首先让我们看看你应该如何找到代理类,尽管代理类被设计为对程序员不可见的,但是使用到代理类的库提供的文档经常会标注出他们的存在,你对你使用的库越熟悉,你就越有可能发现这些代理的使用(The more you’ve familiarized yourself with the basic design decisions of the libraries you use, the less likely you are to be blindsided by proxy usage within those libraries.)
当文档比较短小的时候,头文件可以弥补这个缺陷,因为源代码几乎不可能完全的掩盖代理对象的存在,代理对象通常会从函数的调用中返回(They’re typically returned from functions that clients are expected to call),所以函数的原型反应了他们的存在,这里是std::vector<bool>::operator[]的函数原型
namespace std { // 从C++标准中
template <class Allocator>
class vector<bool, Allocator> {
public:
…
class reference { … };
reference operator[](size_type n);
…
};
}
假定你知道std::vector<T>的[]运算符应该返回一个T&对象,[]运算符意外的返回了其他类型的对象通常便会意味着代理类的存在,多关注你使用的函数接口能让你早些发现代理类的存在。
在实践中,很多的开发者只有当他们追踪神秘的编译问题或是调试不正确的单元测试结果时才会发现的代理类的存在。不管你是如何发现他们的,一旦auto被应用,推导出的类型将是代理类的类型而不是被代理的类型,解决的办法不是抛弃auto,auto本身不是问题,问题是auto推导出的类型并不是你想要的类型,解决办法是强制的让它推导出一个不同的类型,我把这个叫做显式的类型初始化语义(explicitly typed initializer idiom)
显式的类型初始化语义包括用auto声明一个变量,但是加上一个你想要auto推导出的初始化类型,下面是如何强迫将highPriority声明为一个bool类型
auto highPriority = static_cast<bool>(features(w)[5]);
这里,features(w)[5]仍然返回一个std::vector<bool>::reference对象,就像之前一样,但是转换将表达式的类型变成了bool,接着auto将它的类型推导为highPriority了,在运行的时候,从std::vector<bool>::operator[]返回的std::vector<bool>::reference对象执行它支持的bool类型的转换,作为转换的一部分,从features返回的std::vector<bool>的指针被解引用(the still-valid pointer to the std::vector<bool> returned from features is dereferenced)。这避免了我们早先的未定义的行为,索引5接着被应用于相应的指针,最终产生bool类型来初始化highPriority。
对于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());
如果你拥有一个float类型的表达式,但是你把它储存为一个整型的变量,也可以使用这个方法,假定你有一个带有随机访问迭代器(e.g., a std::vector, std::deque,or std::array)的容器,和一个在0-1之间的double类型来暗示元素离容器的开始有多远(0.5暗示了在容器的中间),最终的目的是计算获得这个元素的下标,如果你确定最终的结果不会超过int的范围,如果容器是c,double是d,你可以这样计算下标:
int index = d * c.size();
但是这并没有很好的体现出你有意的将右端的double转换为int,显式的类型初始化语义会让事情变的更加透明
auto index = static_cast<int>(d * c.size());
请记住
- 不可见的代理类会导致auto从初始化表达式中推导出“错误”的类型。
- 显式的类型初始化语义会迫使auto推导出你想要的类型。