- 笔者注:本Item是对上一Item的补充,介绍了
auto
不如预期的可能情况以及解决方法(不是直接对变量用显式类型标识符哦) - 对于
std::vector
,如果存储的数据类型是bool
,那么通过operator[]
索引取到的数据类型不是按照一般规则的bool&
,而是std::vector<bool>::reference
(只有bool
这一种例外)。如果使用auto
推导,就会得到错误的变量类型。
- 这是因为
std::vector<bool>
是逐位(bit)储存的,然而C++不允许对位的引用。因此,std::vector<bool>
返回的实际是一个行为类似bool
的对象,这在设计模式上被称为代理模式(Proxy)。为此,std::vector<bool>::reference
必须在任何bool&
能出现的地方都能顶替。作为等式右侧时(即b0
的定义式),它应该提供一个隐式的转换,又由于这里不能用bool&
,所以它实际上是一个向bool
的转换。 - (笔者注:这里延申一些,在微软的STL实现中,查看源码可以看出
reference
类内部持有一个指针,并对operator=
和operator bool()
做了重载。operator bool()
是一个表达式运算的结果,所以是bool
而非bool&
。当用bool
向reference
赋值时直接用指针进行位运算,用另一个reference
类赋值时则先转成bool
再调用上一种情况。
- 抛开以上那些细节,如果我们毫不知情地使用了
auto
,会怎么样?假设有以下代码:
std::vector<bool> someFunc()
{
return std::vector<bool>{ true, true, false };
}
int main()
{
bool b0 = someFunc()[0];
auto b1 = someFunc()[0];
b0 = false;
cout << "ok here" << endl;
b1 = false;
cout << "still ok?" << endl;
return 0;
}
someFunc()
返回一个std::vector<bool>
的临时对象,b0
通过operator bool()
将值转移到一个新的bool
变量中,没有问题;但b1
保持的reference
对象的指针指向的临时对象在赋值语句结束时已被摧毁,下一句再赋值就会导致undefined behavior!
VS中的运行结果。MSVC的STL实现还是通过大量使用断言进行了保护,没有使程序直接卡死。
- 除开这种情况,代理类还有非常广泛的使用(作者还举了一个模板元编程中使用 expression template 进行矩阵运算的例子)。一般来说这些代理类是我们作为上层使用者不希望直接接触到的,但可惜当使用
auto
时,我们就可能会意外创建这些变量,并且导致像上面bool
例子中的严重问题。因此,避免写下以下形式的代码:
auto someVar = expression of "invisible" proxy class type;
-
那么如何发现代理类的使用?一旦发现,我们又只能抛弃那么香的
auto
了吗? -
第一个问题,两种途径:第一,虽然代理类一般被设计为不那么容易接触到,它们还是会被记录在文档中。越熟悉你用的库的底层设计,就越不容易踩到这类坑里。第二,如果没有文档,可以观察头文件,代理类毕竟还是会出现于源码中,很可能在
return
语句中。(笔者注:我来加个方法三吧:遇到不熟悉的函数或库,用auto
声明后鼠标悬浮看一下变量的类型,如果看着很奇怪,就要点进去看看函数源码了。) -
第二个问题,答案是不必抛弃
auto
。这里作者提出的观点是使用显式类型的初始化器(explicitly typed initializer idiom),由我们帮助auto
找出正确的推导类型。具体代码也很简单:
auto b1 = static_cast<bool>(someFunc()[0]);
- 作者认为,这种语法能明确地表示出我们希望的变量结果,这不限于代理类的问题。例如下面的例子中,我们要计算一个宽限值
ϵ
\epsilon
ϵ,现有的函数返回的是
double
,但对我们来说float
的精度就够用了。那么比较以下两种写法:
double calcEpsilon(); // 计算epsilon,返回double型
float ep = calcEpsilon(); // 是不小心写错了类型,还是我们真的想要float值?
auto ep = static_cast<float>(calcEpsilon()); // 语义明确,我们就是要将返回的double转成float值
- 比较两种写法,很明显作者提出的后一种会使程序表达的语义更加明确。
总结
- “隐形的”代理类可能导致
auto
推导出错误的初始化表达式类型。 - 使用 explicitly typed initializer idiom 让
auto
推导出你想要的类型。