Effective Modern C++ 条款6 当auto会推断出不合理的类型时使用显式类型初始化语法

当auto会推断出不合理的类型时使用显式类型初始化语法

条款5中我们说明了使用auto声明类型会比显式声明类型更好,但是有时候auto类型推断会和我们想象中有点差别。例如,我有一个函数以Widget 为参数,返回std::vector<bool>,容器中每个bool表示Widget是否提供特别的性质:
std::vector<bool> features(const Widget &w);
然后,如果第5个元素代表Widget的高属性的话,我们可以写出下面的代码:

Widget w;
...
bool highPriority = features(w)[5];  // w是否具有高属性?
...
processWidget(w, highPriority);  // 根据w的高属性来处理它

这代码没有错误,它可以运行得很好。但是如果我们做一个看似无关紧要的改变,用auto声明代替显式类型声明:
auto highPriority = features(w)[5];

现在情况改变了,代码编译可以通过,但是代码不会像预期那样运行了:
processWidget(w,highPriority); // 未定义行为

就像注释所说的那样,processWidget函数现在的行为是未定义的。为什么呢?答案可能会让你感到惊讶,在auto的那代码中,highPriority 变量的类型不再是bool类型了。尽管std::vector<bool>概念上是持有bool类型,但是std::vector<bool>operator[]函数返回的并不是容器中元素的引用(除了std::vector<bool>std::vector::operator[]都会返回容器内元素的引用)。与之代替的是,它返回std::vector<bool>::reference类型(std::vector<bool>中的嵌套类)。

std::vector<bool>::reference的存在是因为std::vector<bool>容器是以位(bit)的方式来存储bool变量。这就引起了operator[]函数出现问题,因为std::vector<T>operator[]函数应该是返回T&类型,但是C++无法引用位bit。因此std::vector<bool>operator[]函数无法返回bool&,只能返回一个行为类似(acks like)bool&的对象,这个对象必须可以用于所有需要bool&的地方。在这些情况下,std::vector<bool>::reference会隐式转换为bool类型(不是bool&)。

请记住这个特性,然后重新看看我们一开始的代码:
bool highPriority = features(w)[5];

features 函数返回一个std::vector<bool>对象,然后该对象调用了operator[]函数,这个函数返回一个std::vector<bool>::reference对象,接着该对象隐式转换为一个bool类型变量来初始化highPriority。最终highPriority 的值为features 函数返回的std::vector<bool>对象的第五个元素的值,就是代码看上去那样。

对比用auto声明的highPriority
auto highPriority = features(w)[5]; // 类型推断

同样地,features函数返回一个std::vector<bool>对象,然后该对象调用了operator[]函数,这个函数返回一个std::vector<bool>::reference对象,但这次不一样,因为highPriority 的类型是通过auto推断的,highPriority 的值不再是features 函数返回的std::vector<bool>对象的第五个元素的值了。

highPriority 的值取决于std::vector<bool>::reference的实现。一种实现是该对象内有一个指向容器内字(word)数据结构的偏移量特定个位(bit)数的指针。

feature 函数返回的是个临时的std::vector<bool>对象,这个对象没有名字,为了方便讨论我们称它为temptemp调用了它的operator[]函数,返回的std::vector<bool>::reference对象包含了一个指向字(word)的偏移量为5的指针。所以highPriority 是这个std::vector<bool>::reference对象的拷贝,那么highPriority内也有一个指针,指向temp中的字数据(word)偏移量为5的位。当声明定义highPriority结束后,temp被析构,那么highPriority中的指针就变成了空悬指针(dangling pointer),这就造成了调用processWidget 时产生未定义行为:
processWidget(w, highPriority); // 未定义行为
// highPriority 内含有空悬指针

std::vector<bool>::reference是代理类(proxy class)的一个例子:一个类的存在是为了模仿和增强另一个类的行为。代理类在很多情况下被使用,例如,std::vector<bool>::reference的存在是为了给你一个假象,让你以为std::vector<bool>operator[]函数返回的是位bit的引用;再比如说标准库中的智能指针接收原生指针的内存管理。代理类的使用是根深蒂固的,事实上,代理模式也是一个长时间被褒奖的设计模式。

一些代理类的设计是被用户使用的,例如std::shared_ptrstd::unique_ptr,而另一些代理类的设计是默默工作的,例如std::vector<bool>::reference,它的同胞是std::bitset::reference

与它们同一阵营的还有C++库中一些使用表达式模板(expression templates)的类。开发这些库文件是为了写出高效数字化的代码。例如,一个矩阵类Matrix 以及4个Matrix 对象m1, m2, m3, m4,给定下面的表达式:
Matrix sum = m1 + m2 + m3 + m4;

如果Matrix 对象的operator+函数返回的是一个代理类,而不是返回Matrix,那么计算的效率可以高很多,比如operator+函数返回一个像Sum<Matrix,Matrix>的代理类来替代Matrix。就像std::vector<bool>::referencebool那样,代理类也可以隐式转换为Matrix,也可以通过“=”来初始化sum(本人不懂模板元编程)。

总结一条规则,auto应付不好那些不想被用户知道的代理类。这里代理类的对象通常都是声明表达式结束后就析构(作为临时变量),所以创建这种类型的变量是背离了这些库设计的初衷。例如std::vector<bool>::reference这个例子就违反了这个初衷,从而引发了未定义行为。

所以你应该避免这样的代码:
auto someVar = expression of "invisible" proxy class type;

但是你怎么知道哪些是代理类呢,程序是不会显示它们的实体的,他们应该是不可见的,至少在概念上。就算你真的发现了他们,那么你真的会放弃条款5所说的auto的优势吗?

我们先来解决如何发现它们这个问题。尽管代理类在程序设计中是不可见的,但是库文件会有文档告知。你对你使用的库文件的设计越熟悉,你使用库中代理类的遇到的坑就越少。

如果库文档很让人失望,那么看头文件来弥补。源代码完全遮掩代理类是很少可能发生的,代理类通常是因为客户调用某个函数而返回的,所以调用函数的签名会反应出它们的实体。例如,这是std::vector<bool>::operator[]的说明:

namespace std {
  template <class Allocator>
  class vector<bool, Allocator> {
  public:
  ...
  class reference { ... };
  reference operator[](size_type n);
  ...
  };
}

假定你已经知道std::vector<T>operator[]函数正常会返回T&,那么这种非常规的返回一个代理类会是一种警告。仔细留意你使用的接口,你会经常发现代理类的实体。

在实践中,大部分开发者只会在追查模糊的编译错误或者修改单元测试的bug时才会发现代理类型的使用。不管你是怎么发现它们的,我想说的只要auto推断出的类型是代理类,而不是代理类所代理的类,那么解决办法不一定需要放弃使用autoauto本身是没有问题的,问题只是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,然后autohighPriority 的类型推断为bool

在矩阵Matrix 的那个例子中,我们可以这样写:
auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);

这种语法的应用不限于初始值为代理类这种情况。这语法还可以用于你故意创建一个与初始表达式类型不一样的变量。例如,你有个函数计算可容忍误差值:
double calcEpsilon();

这个函数明确返回double,但是你知道你的应用程序只需要float精确度就足够了,可是值得担心doublefloat的所占字节大小不同。你可以声明一个float变量来存储calcEpsilon函数的结果,
float ep = calcEpsilon(); // 隐式转换

但是这样是很难告诉他人:”我是故意减少函数返回结果的精度的“。但是用显示类型初始化语法可以做到这个意思:
auto ep = static_cast<float>(calcEpsilon());

同样地,你也可以使用它当你故意把float的值附给整形数。再比如,一个double值,范围是0.0~1.0,代表需要的元素与容器首元素的距离(0.5为容器中间的元素),也就是说你需要计算具有随机访问迭代器的容器(例如,std::vectorstd::deque)中的元素的索引。再进一步说,你需要的索引结构类型是int,那么假如容器为cdouble值为d,那么你可以计算索引:
int index = d * c.size();
但是你故意将右边的double转换为int这个意图是模糊的,使用显示类型初始化语义可以清楚地表达这个意图:
auto index = static_cast<int>(d * c.size());

总结

需要记住的2点:

  • 不可见的代理类初始表达式可能会使auto推断出错误的结果
  • 显示类型初始化语法迫使auto推断你想要的类型
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值