Sunday, 27. August 2017 09:47PM
# Item 6: Use the explicitly typed initializer idiom when auto deduces undersired types
#条目6:当auto推断出不希望得到的类型时,使用explicitly typed initializer idiom(显式类型初始化器惯用法)

##术语&概念
- ###explicitly typed initializer idiom
显式类型初始化器惯用法
在初始化时显式类型转换以改变auto的类型推断结果的习惯用法

Example
~~~
double calcEpsilon();
auto ep = static_cast(calcEpsilon()); ~~~

##快速笔记

  1. auto自动类型推断有时候会得到我们不希望看到的结果。什么时候?当初始化表达式返回一个带隐式转换的代理类(Proxy Class)对象的时候。

    • operator[] for std::vector<bool>
    • std::vector<bool>::reference
    • operator+ for Matrix sum = m1 + m2 + m3 + m4;
  2. 如何觉察到这样的不可见的代理类(Proxy Class)?寻找头文件接口的蛛丝马迹。实践中,大多数开发者只有在追踪调试一些令人迷惑的编译问题或调试不正确的单元测试结果的时候才发现Proxy Class被使用了。

  3. 解决这个问题要使用explicitly typed initializer idiom.

  4. explicitly typed initializer idiom 除了可以解决上述问题以外。还有强调这里的类型转换是经过审慎的考虑的作用。

##例子
==Example 1==
我们先来看没有问题的使用方法
~~~
std::vector features(const Widget& w);

Widget w;

bool highPriority = features(w)[5]; //is w high priority?

processWidget(w, highPriority); //process w in accord
//with its priority
~~~
此时我们用auto替代highPriority的显式类型,这个改动看上去似乎无害。
~~~
auto highPriority = features(w)[5];

processWidget(w, highPriority);
~~~
但情况发生了改变,所有的代码会继续编译成功,但是processWidget的行为不再可预期。

为什么?答案可能令人感到惊奇。因为这种情况下highPriority的类型不再是bool。虽然从概念上说std::vector<bool>持有一些bool变量。但是std::vector<bool>operator[]并不返回这个容器中的一个元素的引用(对于其他每个类型,std::vector::operator[]都会返回这样的引用,唯独bool除外)。作为替代,它反回了一个std::vector<bool>::reference对象。(是内嵌(nested)在std::vector<bool>里的一个类)

std::vector<bool>::reference之所以会存在,是因为std::vector<bool>被指定用一种紧密形式(packed form)来表示bool值,每一个bit代表一个bool值。这个做法给operator[]制造了一个问题:std::vector<T>::operator[]应该返回T&,但是这里的元素实际是个bit,C++禁止对bit的引用。所以返回bool&是不行了,只能返回一个对象让它表现起来像一个bool&。关于这个技术的所有细节,此处按下不表。比喻地说,这里的隐式转换只是一幅更大的马赛克嵌画中的其中一块。

我们再来比较例子中的情况
~~~
bool highPriority = feature(w)[5]; //declare highPriority’s type explicitly
~~~
等式右边返回了一个std::vector<bool>::reference并且隐式转换了成了一个bool。highPriority确实的保存了第5个bit的值。
再看
~~~
auto highPriority = features(w)[5]; //deduce highPriority’s type
~~~
等式右边反回了一个std::vector<bool>::reference并且auto直接推断highPriority就是这个类型,因此没有bool对象被构造出来。
这里highPriority是什么值取决于std::vector<bool>::reference是如何实现的。一种实现是,这个对象持有一个指向机器字节(machine word)的指针,这个字节持有被引用的位(bit)。考虑到highPriority初始化的含义(jtpan:接受一个引用),我们就假设这里的实现是内存就地(in place)的。
等号右边的feature调用返回了一个临时容器,然后临时容器将包含了上述指针的reference对象返回给了highPriority,通过偏移量reference可以从那个字节中找到第五位(bit)。当这个语句调用结束,临时对象销毁,然后highPriority就仅仅包含一个空悬的指针(dangling pointer)了。这就是导致调用了processWidget的如下语句是未定义行为(undefined behavior)的原因。
~~~
processWidget(w, highPriority); // undefined behavior!
// highPriority contains dangling pointer!
~~~

std::vector<bool>::reference就是一个代理类(proxy class): 为了模仿(emulating)和扩张(augmenting)其他某些类的行为而存在的类。
事实上,设计模式“Proxy”是在软件设计模式万神殿中屹立最久的成员之一。

在一些使用了表达式模板(expression templates)技术的C++库中,我们也可以看到这样的代理类。那样的库一开始就是为了改进数值计算的效率而被开发的。
~~~
Matrix sum = m1 + m2 + m3 + m4;
~~~
如果operator+返回的是计算结果的代理类而不是结果本身的话,这个矩阵计算会有效率的多。(jtpan:为什么有效率?大概是因为lazy evaluation)
例如返回Sum<Matrix,Matrix>, 就像bool那个例子里的reference一样,这个计算结果的代理类可以隐式转换成Matrix。这样等式右边的实际类型就是Sum<Sum<Sum<Matrix,Matrix>,Matrix>,Matrix>, 这肯定是一个应该向客户(clients)隐藏的类。

一个一般性的准则是,“不可见的(invisible)”的代理类(proxy class)不能很好的与auto一起工作。那样的类通常不被设计成能活过一个单语句(single statement),所以创建那样的类的一个对象变量已经趋向于打破了基本的库设计假设(library design assumptions)。我们看到打破那样的假设会带来未定义行为。

因此我们要避免如下形式的代码:
~~~
auto someVar = expression of “invisible” proxy class type
~~~

==Example 2==
那我们应该怎么样发现这种“不可见”的代理类呢?就从接口中寻找蛛丝马迹。
~~~
namespace std {

template class vector<bool, Allocator> { ... class reference {...}

reference operator[](size_type n);   }; } ~~~ 假设你知道`std::vector<T>::operator[]`通常会返回一个T&, 那这里不符合惯例的返回值就提示了你有个proxy class 被使用了。

==Example 3==
一旦auto被决定用来推断出代理类的类型而不是被代理的类型的时候,就已经表明了解决这个问题的方案不会是放弃使用auto。auto本身没有问题。问题在于auto没有推断出你想要的类型。解决方案就是强迫他作一个不同的类型推断。这个方法被我们称为the explicitly typed initializer idiom(显式类型初始化器惯用法)。例子如下
~~~
auto highPriority = static_cast(features(w)[5]); ~~~ 在Matrix的例子中 ~~~ auto sum = static_cast(m1 + m2 + m3 + m4); ~~~

==Example 4==
这种用法同时可以强调你是经过了审慎地(deliberately)考虑之后才决定创建一个类型不同于初始化表达式类型的的变量。
~~~
double calcEpsilon(); //return tolerance value
~~~
如果你知道这里float的精度足够并且你确实在意float和double之间的大小差别,那么你可以声明一个float变量去保存返回结果,像这样:
~~~
float ep = calcEpsilon(); //implicity convert double->float
~~~
这很难说有表达出你“审慎地(deliberately)将函数的返回结果精度降为float”的意思。
但是像下面这样写,你的意思就到位了。
~~~
auto ep = static_cast(calcEpsilon()); ~~~

类似的例子:
你需要计算一个元素在有随机访问迭代器(random access iterators)的容器中的索引值(index)。你将被给予一个介于0.0和1.0之间的double数值d来指明该元素坐落的位置距离容器开头有多远。假设你很确信容器c不会为空。
~~~
int idx = d * c.size();
if (idx == c.size()) –idx; //keep idx valid when d == 1.0
~~~
但是这种写法将你是故意将double转换成int的事实变得模糊不清。如果这里使用explicitly typed initializer idiom,那么事情就变得显然易懂。
~~~
auto idx = static_cast(d * c.size()); if (idx == c.size()) --idx; //keep idx valid when d == 1.0 ~~~

##谨记要点
- “不可见”的代理类型 会导致auto针对初始化表达式推断出“错误的”类型。(jtpan:不是错误,只是不符合你的希望)。
- explicitly typed initializer idiom强迫auto去推断出你希望他推断出的类型。