《Effective Modern C++》学习笔记 - Item 6: 当auto推导出非预期类型时,使用“显式类型初始化器”

  • 笔者注:本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&。当用 boolreference 赋值时直接用指针进行位运算,用另一个 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值
  • 比较两种写法,很明显作者提出的后一种会使程序表达的语义更加明确。

总结

  1. “隐形的”代理类可能导致 auto 推导出错误的初始化表达式类型。
  2. 使用 explicitly typed initializer idiom 让 auto 推导出你想要的类型。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值