Effective Modern C++翻译(7)-条款6:当auto推导出意外的类型时,使用显式的类型初始化语义

条款6:当auto推导出意外的类型时,使用显式的类型初始化语义

条款5解释了使用auto来声明变量比使用精确的类型声明多了了很多的技术优势,但有的时候,当你想要zag的时候,auto可能会推导出了zig。例如,我有一个函数,它以const Widget&作为参数,并且返回std::vector<bool>,每一个bool暗示了Widget是否提供了一个特殊的特性。

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

进一步假设第5个bool暗示了Widget是否拥有比较高的优先级,我们可以写下这样的代码。

Widget w;
…
bool highPriority = features(w)[5]; // w是否有较高的优先级?
…
processWidget(w, highPriority);    //根据w是否拥有较高的
                                         //的优先级来对它进行处理

这段代码没有任何问题,它会很好的工作,但是如果我们声明highPriority时用看起来无害的auto代替精确的类型声明

auto highPriority = features(w)[5]; // w是否有较高的优先级?

在这种情况下,所有的代码都会编译成功,但是它的行为却是未定义的:

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

就像注释指出的那样,对processWidget的调用行为现在是未定义的了,但是为什么呢,答案可能会十分令人惊讶,在使用auto的代码中,highPriority的类型不再是bool,尽管std::vector<bool>概念上应该持有bool对象,但[]运算符并不返回容器内元素的引用(std::vector::operator[]返回容器的每一个类型除了bool),相反它返回一个std::vector<bool>:reference类型的对象(std::vector<bool>中的内部类)

 

std::vector<bool>::reference的存在是因为std::vector<bool>内部用一种紧缩的形式来表示bool对象,每一个bit代表一个bool对象,这使得std::vector<bool>的[]运算符出现了问题,因为std::vector<T>的[]运算符应该返回T&类型的对象,但是C++禁止返回对位对象的引用。无法返回bool&,std::vector<bool>的[]运算符返回了一个对象,它的行为看起来很像bool&,为了让这个想法能够成功,std::vector<bool>::reference对象必须能够在bool&都够使用的地方同样适用,在features中,std::vector<bool>::reference实现这个工作是通过一个到bool的隐式转换(不是bool&到bool,为了完整的解释std::vector<bool>::reference模拟bool&行为中使用的技术将会将我们带的太远太远,所以我简单的说这个隐私的转换只是很小的一部分(I’ll simply remark that this implicit conversion is only one stone in a larger mosaic)

带着这个思想,我们再来看一看最初的代码

bool highPriority = features(w)[5]; //精确声明highPriority
                                    //的类型

这里,features返回了一个std::vector<bool>对象,并在这个对象上调用了[]运算符,[]运算符返回了一个std::vector<bool>::reference对象,这个对象为了初始化highPriority对象被隐式的转化为了一个bool对象。highPriority因此最终通过features获得了std::vector<bool>中第5个bit的值,就像它本应该的那样。

 

对比一下如果用auto声明highPriority会发生什么呢?

 

features返回了一个std::vector<bool>对象,[]运算符作用在了上面,[]继续返回一个std::vector<bool>::reference对象,但是现在有了一点改变,因为使用了auto来声明highPriority的类型,highPriority并不拥有features返回的std::vector<bool>对象的第5个bit的值。

 

highPriority的值取决于std::vector<bool>::reference是如何实现的,一种实现方式是std::vector<bool>::reference包含一个指针指向机器字,加上对引用位的偏移,我们考虑一下如果std::vector<bool>::reference是这样实现的,highPriority的初始化意味着什么。

 

对features的调用返回了一个临时的std::vector<bool>对象,这个对象没有名字,但是为了方便讨论,我这里叫它temp,[]运算符在temp上调用,返回的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>::reference的[]运算符返回了一个对位的引用的错觉,标准库的智能指针类型(参见第4章)移植了裸指针的资源管理(the Standard Library’s smart pointer types (see Chapter 4) are proxy classes that graft resource management onto raw pointers. The)。代理类的特性已经被广泛的建立了,事实上在设计模式的宫殿中代理模式是存在时间最长的成员之一。

 

一些代理类对客户来说是很显然的,例如std::shared_ptr和std::unique_ptr,而另一些代理类被设计的或多或少有些不可见,例如std::vector<bool>::reference,std::bitset::reference。

 

同样C++中一些库库中的类使用了一种叫表达式模板的东西,这些库早先的目的是为了提高数字运算(numeric code)的效率,假定有一个Matrix类和4个Matrix对象,m1,m2,m3,m4。

Matrix sum=m1+m2+m3+m4

如果+运算符返回一个结果的代理而不是结果本身的话,运算会更有效率。两个Matrix对象的+可以返回一个代理类,例如Sum<Matrix,Matrix>而不是Matri对象本身。和std::vector<bool>::reference和bool的例子一样,代理类和Matrix之间会有一个隐私的转化,允许代理对象初始化等号右边的sum对象(初始化对象的表达式可能会是Sum<Sum<Sum<Matrix,Matrix>,Matrix>,Matrix>,这个类型肯定需要对客户隐藏起来)

照例,不可见的代理类和auto间相处的并不是很好,这些代理类通常被设计为不会存活超过一条语句,所以创建这样类型的变量违背了基础库的设计假设,就像std::vector<bool>::reference,我们可以看到违背这样假设会引发未定义的行为。

 

因此你会想要避免这样形式的代码:

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

但是你应该如何识别代理类呢,使用到代理类的代码不太可能会突显出他们的存在,他们至少在概念上是不可见的,一旦你发现他们,难道你应该抛弃auto和条款5提到的auto带来的大量优点吗?

首先让我们看看你应该如何找到代理类,尽管代理类被设计为对程序员不可见的,但是使用到代理类的库提供的文档经常会标注出他们的存在,你对你使用的库越熟悉,你就越有可能发现这些代理的使用(The more you’ve familiarized yourself with the basic design decisions of the libraries you use, the less likely you are to be blindsided by proxy usage within those libraries.)

当文档比较短小的时候,头文件可以弥补这个缺陷,因为源代码几乎不可能完全的掩盖代理对象的存在,代理对象通常会从函数的调用中返回(They’re typically returned from functions that clients are expected to call),所以函数的原型反应了他们的存在,这里是std::vector<bool>::operator[]的函数原型

namespace std { // 从C++标准中
template <class Allocator>
class vector<bool, Allocator> {
    public:
    …
    class reference { … };
    reference operator[](size_type n);
    …
};
}

假定你知道std::vector<T>的[]运算符应该返回一个T&对象,[]运算符意外的返回了其他类型的对象通常便会意味着代理类的存在,多关注你使用的函数接口能让你早些发现代理类的存在。

在实践中,很多的开发者只有当他们追踪神秘的编译问题或是调试不正确的单元测试结果时才会发现的代理类的存在。不管你是如何发现他们的,一旦auto被应用,推导出的类型将是代理类的类型而不是被代理的类型,解决的办法不是抛弃auto,auto本身不是问题,问题是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,接着auto将它的类型推导为highPriority了,在运行的时候,从std::vector<bool>::operator[]返回的std::vector<bool>::reference对象执行它支持的bool类型的转换,作为转换的一部分,从features返回的std::vector<bool>的指针被解引用(the still-valid pointer to the std::vector<bool> returned from features is dereferenced)。这避免了我们早先的未定义的行为,索引5接着被应用于相应的指针,最终产生bool类型来初始化highPriority。

对于Matrix这个例子,显式的类型初始化语义将会像这样:

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

 

这个应用并没有局限于会产生代理类的初始化,它同时也适用当你想强调你创造的变量的类型不同于初始化的表达式的时候,例如假如你有一个计算公差值的函数

double calcEpsilon(); // 返回公差值

calcEpsilon返回的类型是double,但是假定你知道对于你的应用float的精度就已经足够了,你更关心float和double在大小上的不同,所以你声明了一个float变量来储存calcEpsilon的结果。

float ep = calcEpsilon(); // 隐式的
                          // 将double转换为float

但是这个并没有说明我有意的改变了函数返回的类型,而使用显式的类型初始化语义可以:

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

 

如果你拥有一个float类型的表达式,但是你把它储存为一个整型的变量,也可以使用这个方法,假定你有一个带有随机访问迭代器(e.g., a std::vector, std::deque,or std::array)的容器,和一个在0-1之间的double类型来暗示元素离容器的开始有多远(0.5暗示了在容器的中间),最终的目的是计算获得这个元素的下标,如果你确定最终的结果不会超过int的范围,如果容器是c,double是d,你可以这样计算下标:

int index = d * c.size();

但是这并没有很好的体现出你有意的将右端的double转换为int,显式的类型初始化语义会让事情变的更加透明

auto index = static_cast<int>(d * c.size());

 

请记住

  • 不可见的代理类会导致auto从初始化表达式中推导出“错误”的类型。
  • 显式的类型初始化语义会迫使auto推导出你想要的类型。
时间: 2024-08-27 23:54:04

Effective Modern C++翻译(7)-条款6:当auto推导出意外的类型时,使用显式的类型初始化语义的相关文章

[Effective Modern C++] Item 6. Use the explicitly typed initializer idiom when auto deduces undesired types - 当推断意外类型时使用显式的类型初始化语句

条款6 当推断意外类型时使用显式的类型初始化语句 基础知识 当使用std::vector<bool>的时候,类型推断会出现问题: std::vector<bool> features(const Widget& w); // OK bool highPriority = features(w)[5]; processWidget(w, highPriority); // ERROR auto highPriority = features(w)[5]; processWid

Effective Modern C++翻译(4)-条款3

条款3 了解decltype decltype是一个有趣的东西,给它一个变量名或是一个表达式,decltype会告诉你这个变量名或是这个表达式的类型,通常,告诉你的结果和你预测的是一样的,但是偶尔的结果也会让你挠头思考,开始找一些参考资料进行研究,或是在网上寻找答案.   我们从典型的例子开始,因为它的结果都是在我们预料之中的,和模板类型推导与auto类型推导相比(参见条款1和条款2),decltype几乎总是总是返回变量名或是表达式的类型而不会进行任何的修改 const int i = 0;

Effective Modern C++翻译(5)-条款4

条款4:了解如何观察推导出的类型 那些想要知道编译器推导出的类型的人通常分为两种,第一种是实用主义者,他们的动力通常来自于软件产生的问题(例如他们还在调试解决中),他们利用编译器进行寻找,并相信这个能帮他们找到问题的源头(they're looking for insights into compilation that can help them identify the source of the problem.).另一种是经验主义者,他们探索条款1-3所描述的推导规则,并且从大量的推导情

Effective Modern C++翻译(2)-条款1

第一章 类型推导 C++98有一套单一的类型推导的规则:用来推导函数模板,C++11轻微的修改了这些规则并且增加了两个,一个用于auto,一个用于decltype,接着C++14扩展了auto和decltype可以使用的语境,类型推导的普遍应用将程序员从必须拼写那些显然的,多余的类型的暴政中解放了出来,它使得C++开发的软件更有弹性,因为在某处改变一个类型会自动的通过类型推导传播到其他的地方.   然而,它可能使产生的代码更难观察,因为编译器推导出的类型可能不像我们想的那样显而易见.   想要在

Effective Modern C++翻译(3)-条款2

条款2 明白auto类型推导 如果你已经读完了条款1中有关模板类型推导的内容,那么你几乎已经知道了所有关于auto类型推导的事情,因为除了一个古怪的例外,auto的类型推导规则和模板的类型推导规则是一样的,但是为什么会这样呢?模板的类型推导涉及了模板,函数和参数,但是auto的类型推导却没有涉及其中的任何一个. 这确实是对的,但这无关紧要,在auto类型推导和template之间存在一个直接的映射,可以逐字逐句的将一个转化为另外一个. 在条款1中,模板类型推导是以下面的模板形式进行举例讲解的:

《Effective Modern C++》读书笔记 Item 2 auto的类型推导

注意: 还要学习一个 ↑↑↑↑ 这样的方框里的片段完全不来自于原书,而是我自己的理解. Item 2 Understand auto type deduction - auto类型推导 在C++11之前,auto 关键字一直是用于声明自动储存类型的变量时使用的,基本上没有什么实际作用,地位和 export 关键字(用于向编译单元之外导出模板,也在C++11中被取消)类似. 在C++11中,auto 终于不再废材,终于具备了类似C#中 var 关键字的效果,可以自动推导出变量的类型,可以少打几个字

《Effective Modern C++》翻译--条款3: 理解decltype

条款3:理解decltype decltype 是一个非常有趣的怪兽.如果提供了一个名字或是表达式,decltype关键字将会告诉你这个名字或这个表达式的类型.通常情况下,结果与你的期望吻合.然而有些时候,decltype产生的结果领你挠头,使你去翻阅参考书或在网上问答中寻求答案. 我们先从通常的情况开始-这里没有暗藏惊喜.联系到在模板类型推导和auto类型推导中发生了什么,decltype关键字就像鹦鹉学舌一样,对于变量名称或表达式类型的推导跟模板类型推导和auto类型推导没有任何变化: co

《Effective Modern C++》要点中英文对照

目录 CHAPTER 1 Deducing Types 章节1 类型推导 Item 1:Understand template type deduction. 条款1:理解模板类型推导. Item 2:Understand auto type deduction. 条款2:理解auto类型推导. Item 3:Understand decltype. 条款3:理解decltype. Item 4:Know how to view deduced types. 条款4:知道如何查看推导出来的类型.

《Effective Modern C++》翻译--条款1: 理解模板类型推导

北京2016年1月9日13:47:17 开始第一章的翻译. 第一章名为 类型推断 分为四个条款: 1理解模板类型推导 2理解auto自动类型推导 3理解decltype操作符 4如何对待推导的类型 第一章 类型推导 C++98有一套单一的类型推导的规则用来推导函数模板.C++11轻微的修改了这些规则并且增加了两个推导规则,一个用于auto,一个用于decltype.接着C++14扩展了auto和decltype可以使用的语境.类型推导的普遍应用将程序员从必须拼写那些显然多余的类型中解放了出来,它