条款6:当auto推导的型别不符合要求时,使用带显示型别的初始化物习惯用法

条款5解释了使用auto声明变量相对于显式指定型别而言,可以带来若干技术上的优势。但是有的情况下,你想往东,auto型别推导的结果偏偏往西。举个例子,假设我有一个函数接受一个Widget并返回一个std::vector<boo>,其中每一个bool元素都代表着Widget是否提供一种特定功能:

std::vector<bool> features(const Widget& w);

再假设,这里面的第5个比特位代表的意思是:Widget是否具有高优先级。所以我们会写出下面的代码:

Widget w;
bool hightPriority = features(w)[5];  // w具有高优先级吗?
processWidget(w, highPriority);       // 按照w的优先级来处理

这段代码没有问题,会运行的很好。不过,只要我们做一个看似无害的改变,把highPriority从显式型别改成auto,

auto highPriority = features(w)[5];  // w具有高优先级吗?

情况顿时急转直下,所有的代码仍然可以编译,但是其行为则变得不再可以预期了:

processWidget(w, highPriority); // 未定义行为!

答案有点儿出乎意料。在那段改用了auto的代码中,highPriority的型别不再是bool了。尽管从概念上说,std::vector<bool>应该持有的是bool型别的元素,但std::vector<bool>的operator[]返回值并不是容器中的一个元素的引用(对于其他所有形参型别而言,std::vector::operator[]都返回这样的值,单单bool是个例外)。它返回的是个std::vector<bool>::reference型别的对象(这是个嵌套在std::vector<bool>里面的类)。

之所以要弄出个std::vector<bool>::reference,是因为std::vector<bool>做过特化,用了一种压缩形式表示其持有的bool元素,每个bool元素用一个比特来表示。这种做法给std::vector<bool>的operator[]带来了一个问题,因为按理说std::vector<bool>的operator[]应该返回一个T&,然而C++却禁止bit的引用,既然不能返回一个bool&,std::vector<bool>的operator[]转而返回一个表现的像bool&的对象, 而这个把戏若要成功,std::vector<bool>::reference型别的对象就要在所有能用bool&的地方保证它们也能用。实现这个效果的原理是,std::vector<bool>::reference做了一个向bool的隐式型别转换(目标不是bool&,而是bool)。

再看一遍原始代码中的这一句:

bool highPriority = features(w)[5]; //显式声明highPriority的型别

这里,features返回了一个std::vector<bool>对象,然后针对该对象执行operator[]。而后,operator[]返回一个std::vector<bool>::reference型别的对象,该对象被隐式转换为一个初始化highPriority所需的bool对象。所以highPriority的值最终被设定为features所返回的std::vector<bool>对象的第5个比特,一如所愿。

对比一下auto来声明highPriority时所发生的事:

auto highPriority = features(w)[5];  //highPriority的型别由推导而得

和前面一样,features返回了一个std::vector<bool>对象,然后针对该对象执行operator[],尔后,operator[]返回一个std::vector<bool>::reference型别的对象,可是这里开始不一样了。auto会把highPriority的型别推导成std::vector<bool>::reference。这么一来,highPriority的值就完全不可能会是features所返回的std::vector<bool>对象的第5个比特了。

在后一种情况下,highPriority的值取决于std::vector<bool>::reference的实现。有一种实现让对象含有一个指针,指涉到一个机器字(word),该机器字持有那个被引用的比特,再加上基于那个比特对应的字的偏移量。考虑一下,如果std::vector<bool>::reference真的是这样实现的话,highPrority的初始化将会如何完成。

对features的调用会返回一个std::vector<bool>型别的临时对象。该对象没有名字,但未讨论方便,我称之为temp。针对temp执行operator[],返回一个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>的operator[]的错觉,让它返回一个对bit的引用,而且标准库的智能指针也是一些对托管资源的代理类,使得他们的资源管理类似于原始指针。代理类的功能是良好确定的。事实上,“代理”模式是软件设计模式中最坚挺的成员之一。

一些代理类被设计用来隔离用户,这就是std::shared_ptr和std::unique_ptr的情况。另外一些代理类是为了一些或多或少的不可见性。std::vector<bool>::reference就是这样一个“不可见”的代理,和他类似的是std::bitset,对应的是std::bitset::reference。

同时在一些C++库里面的类存在一种被称作表达式模板的技术,这些库刚开始是为了提高数值运算效率,提供一个Matrix类和Matrix对象m1,m2,m3和m4,举一个例子,下面的表达式:

Matrix sum = m1 + m2+ m3 + m4;

可以计算的更快如果Matrix的operator+返回一个结果的代理而不是结果本身,这是因为,对于两个Matrix,operator+可能返回一个类似于Sum<Matrix, Matrix>的代理而不是一个Matrix对象。和std::vector<bool>::reference一样,这里会有一个隐式的从代理类到Matrix的转换,这个可能允许sum由=右边的表达式产生的代理对象进行初始化。(其中的对象可能会编码整个初始化表达式,也就是,变成一种类似于Sum<Sum<Sum<Matrix, Matrix>, Matrix>,Matrix)的类型,这是一个客户端需要屏蔽的类型。)

作为一个通用的法则,“不可见”的代理类不能和auto愉快的玩耍。这种类常常它的生命周期不会被设计成超过一个单个的语句。所以创造这样的类型的变量是会违反库的设计假定。这就是std::vector<bool>::reference的情况,而且我们可以看到这种违背约定的做法会导致未定义的行为。

因此你要避免使用下面的代码形式:

auto sameVar = expression of "invisible" proxy class type;

但是你怎么能知道代理类被使用呢?软件使用它们的时候并不可能会告知它们的存在。它们是不可见的,至少在概念上!一旦你发现了它们,难道就必须放弃使用auto加之条款5所生命的auto的各种好处吗?

我们先看看怎么解决如何发现它们的问题。尽管“不可见”的代理类被设计用来fly beneath programmer raddat in day-to-day use,库使用它们的时候常常会撰写关于它们的文档来解释为什么会这么做。你对你所使用的库的基础设计理念越熟悉,你就越不可能在这些库中被代理的使用搞得狼狈不堪。

当文档不够用的时候,头文件可以弥补空缺。很少有源代码封装一个完全的代理类。它们常常从一些客户调用者期望调用的函数返回,所有函数签名常常可以表征它们的存在。这里是std::vector<bool>::operator[]的例子:

namespace std { 
    // from C++ Standards 
    template <class Allocator>
    class vector<bool, Allocator> 
    { 
    public: 
        …
    class reference { … }; 
    reference operator[](size_type n); 
    … 
    }; 
}
假设你知道对 std::vector<T> operator[] 常常返回一个 T& ,在这个例子中的这种非常规
operator[] 的返回类型一般就表征了代理类的使用。在你正在使用的这些接口之上加以关
注常常可以发现代理类的存在。
在实践上,很多的开发者只会在尝试修复一些奇怪的编译问题或者是调试一些错误的单元测
试结果中发现代理类的使用。不管你是如何发现它们,一旦 auto 被决定作为推导代理类的类
型而不是它被代理的类型,它就不需要涉及到关于 auto auto 自己本身没有问题。问题在
auto 推导的类型不是所想让它推导出来的类型。解决方案就是强制一个不同的类型推导。
我把这种方法叫做显式的类型初始化原则。
显式的类型初始化原则涉及到使用 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> 的指针被间接引用。这样就在运行的 开始避免了未定义行为。索引5然后放置在bits指针的偏移上,然后暴露的 bool 就作为 highPriority 的初始化数值。

针对于 Matrix 的例子,显示的类型初始化原则可能会看起来是这样的:

auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);

关于这个原则下面的程序并不禁止初始化但是要排除代理类类型。强调你要谨慎地创建一个类型的变量,它和从初始化表达式生成的类型是不同的也是有帮助意义的。举一个例子,假设你有一个函数去计算一些方差:

double calcEpsilon();  //返回方差

calcEpsilon明确的返回一个double,但是假设你知道你的程序,float的精度就够了的时候,而且你要关注double和float的长度的区别。你可以声明一个float变量去存储calcEpsilon的结果:

float ep = calcEpsilon();  //隐式转换double到float

但是这个很难表明“我故意减小函数返回值的精度”,一个使用显式的类型初始化原则式这样做的:

auto ep = static_cast<float>(calcEpsilon());

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值