这里列举一个auto会出现推导错误的典型场景,代码如下:
std::vector<bool> features(const Widget& w);
Widget w;
...
bool highPriortiy = features(w)[5]; //w具有高优先级吗?
...
processWidget(w, highPriortiy); //按照w的优先级来处理之
这上面的代码运行起来都很正常,但是将highPriortiy
型别声明的时候换成auto
,则后面就会出现未定义的行为了。
std::vector<bool> features(const Widget& w);
Widget w;
...
auto highPriortiy = features(w)[5]; //w具有高优先级吗?
...
processWidget(w, highPriortiy); //未定义行为!
这里auto
推导出来的型别不再是bool
,而是std::vector<bool>::reference
。之所以要弄出一个std::vector<bool>::reference
是因为std::vector<bool>
在存储的时候,采用了压缩形式的表示方法,每个bool元素有用一个比特来表示。这种优化方式给std::vector<bool>
的operator[]
带来了一个问题。因为按照std::vector<T>
的operator[]
应该返回一个T&
,但C++禁止比特的引用。
那么解决方案是这样的,std::vector<bool>
的operator[]
返回了一个表现的像bool&的对象,即std::vector<bool>::reference
。这里不做细究,大概就是把std::vector<bool>::reference
做了一个向bool
的隐式型别转换。
我们来分析这两节代码的背后逻辑:
-
对于正常情况
非auto
下,features返回了一个std::vector<bool>
对象,然后针对该对象执行operator[]
,返回一个std::vector<bool>::reference
对象。然后此对象隐式转换为初始化highPriority所需的bool对象。 -
对于
auto
情况下,features返回了一个std::vector<bool>
对象,然后针对该对象执行operator[]
,返回一个std::vector<bool>::reference
对象。从这里就不一样了,auto
会把highPriority
的型别推导成std::vector<bool>::reference
。这么一来,后续的路就完全跑偏了,而且这个跑偏后的结果取决于std::vector<bool>::reference
的实现方式。
如果基于第二条继续延伸一下,则有这样的可能性。有一种std::vector<bool>::reference
的实现方式是让对象含有一个指针,指涉到一个机器字(word),该及其子持有的那个被引用的比特,在加上基于那个比特对应的字的偏移量。在这种实现框架下,我们继续探讨。
对features的调用会返回一个std::vector<bool>
型别的临时对象,该对象没有名字,但是我们为讨论方便暂且称呼它为temp
。针对temp
执行operator[]
,返回一个std::vector<bool>::reference
的对象,此对象含有一个指涉到机器字的指针,该机器字在一个持有temp
所管理的那样比特的数据结构中,还要加上在第5个比特所对应的机器字偏移量。由于highPriority
是temp
的一个副本,所以highPriority
也含有一个指涉到temp
中的机器字的指针说了那么多屁话,其实就是浅拷贝啦。但是在表达式结尾处temp
被析构了,因为它是个临时对象!这个时候要命的highPriority
里就有悬空指针啦。
总结:auto在什么时候会出错
auto和 "隐形"代理类无法和平共处
代理类,就是为了模拟或增广其他型别的类。
-
模拟:例如
std::vector<bool>::reference
就是为了制造std::vector<bool>
的operator[]
返回了一个比特引用的假象。这个同样的例子在std::bitset
对应的std::bitset::reference
里也一样. -
增广: 表达式模板技术,提高数值计算代码的效率。
Matrix sum = m1 + m2 + m3 + m4;
这里Matrix对象的
operator+
返回的是结果的代理而不是结果本身,则上述计算会高效很多。
总体说来,我们需要避免如下代码:
auto someVar = "隐形"代理型别表达式;
如何发现“隐形”代理类
因为隐形代理类的设计就是为了隐藏细节,所以一般来说使用过程中设计者是尽可能让人不察觉到。这个时候一般只能从源码/说明文档里找到端倪。
-
文档:使用的库往往会在文档中写明这一点。
-
头文件:假设已经知道
std::vector<T>
的operator[]
返回值是T&,但是看到了返回值不是这样,而是定义了一个新的类型。那么应该是代理类。
对于“隐形”代理类的处理方式
即便发现有隐形代理类的时候,auto
可能不太好用,但也可以用别的方式让auto
继续起效。例如下述方式:
auto highPriority = static_cast<bool>(features(w)[5]);
这种方式必定能能够让代码稳妥的运行,也能使auto
继续起效。
要点速记 |
---|
1. “隐形”的代理型别可以导致auto根据初始化表达式推导出“错误的”型别。 |
2. 带显示型别的初始化习惯用法强制auto推导出你想要的型别。 |