《EffectiveModernC++》第二章:auto

第二章 auto

item5:优先考虑auto而非显式类型声明

1
2
3
4
5
6
7
8
template<typename It>           //对从b到e的所有元素使用
void dwim(It b, It e) //dwim(“do what I mean”)算法
{
while (b != e) {
typename std::iterator_traits<It>::value_type currValue = *b;//auto currValue = *b;即可

}
}

typename std::iterator_traits<It>::value_type是想表达迭代器指向的元素的值的类型,然而我们即使写出 typename std::iterator_traits<It>::value_type也不知道他具体是一个什么类型,只不过是萃取器帮我们找到了对应的valuetype,此时不妨直接使用auto。

1
2
3
4
auto derefUPLess = 
[](const std::unique_ptr<Widget> &p1, //用于std::unique_ptr
const std::unique_ptr<Widget> &p2) //指向的Widget类型的
{ return *p1 < *p2; }; //比较函数

很酷对吧,如果使用C++14,将会变得更酷,因为lambda表达式中的形参也可以使用auto

1
2
3
4
auto derefLess =                                //C++14版本
[](const auto& p1, //被任何像指针一样的东西
const auto& p2) //指向的值的比较函数
{ return *p1 < *p2; };

但是你可能会想我们完全不需要使用auto声明局部变量来保存一个闭包,因为我们可以使用std::function对象。std::function对象到底是什么?

std::function是一个C++11标准模板库中的一个模板,它泛化了函数指针的概念。与函数指针只能指向函数不同,std::function可以指向任何可调用对象,也就是那些像函数一样能进行调用的东西。当你声明函数指针时你必须指定函数类型(即函数签名),同样当你创建std::function对象时你也需要提供函数签名,由于它是一个模板所以你需要在它的模板参数里面提供。举个例子,假设你想声明一个std::function对象func使它指向一个可调用对象,比如一个具有这样函数签名的函数,

1
2
3
bool(const std::unique_ptr<Widget> &,           //C++11
const std::unique_ptr<Widget> &) //std::unique_ptr<Widget>
//比较函数的签名

你就得这么写:

1
2
std::function<bool(const std::unique_ptr<Widget> &,
const std::unique_ptr<Widget> &)> func;

因为lambda表达式能产生一个可调用对象,所以我们现在可以把闭包存放到std::function对象中。这意味着我们可以不使用auto写出C++11版的derefUPLess

1
2
3
4
5
std::function<bool(const std::unique_ptr<Widget> &,
const std::unique_ptr<Widget> &)>
derefUPLess = [](const std::unique_ptr<Widget> &p1,
const std::unique_ptr<Widget> &p2)
{ return *p1 < *p2; };

一个持有闭包(closure)的auto变量具有和闭包(closure)一样的类型,并且因此仅消耗闭包(closure)所需求的内存空间。

一个持有闭包(closure)的std::function变量的类型是std::function模板的一个具现(instantiation),并且它对于任意的函数signature都有固定的内存空间。这个内存空间的大小也许并不满足闭包(closure)的需求,所以std::function的构造函数可能会申请堆内存来存储闭包(closure)。因此,std::function对象通常会比auto对象消耗更多的内存空间。

另外,实现细节禁用inline,会导致间接地函数调用。因此,通过std::function对象调用闭包(closure)几乎肯定会比通过auto对象调用慢。

总之,std::function方法会比auto方法消耗更多空间且执行更慢,并且std::function方法还可能产生out-of-memory的异常。
因此auto几乎在这场竞争中全面胜利,更好写,更好的性能。

我们再来讨论下类型快捷方式(type shortcuts)有关的问题:

1
2
3
std::vector<int> v;

unsigned sz = v.size();

v.size()的标准返回类型是std::vector<int>::size_type,但是只有少数开发者意识到这点。std::vector<int>::size_type实际上被指定为无符号整型,所以很多人都认为用unsigned就足够了,写下了上述的代码。这会造成一些有趣的结果。举个例子,在Windows 32-bitstd::vector<int>::size_typeunsigned是一样的大小,但是在Windows 64-bitstd::vector<int>::size_type是64位,unsigned是32位。这意味着这段代码在Windows 32-bit上正常工作,但是当把应用程序移植到Windows 64-bit上时就可能会出现一些问题。谁愿意花时间处理这些细枝末节的问题呢?

所以使用auto可以确保你不需要浪费时间:

1
auto sz =v.size();                      //sz的类型是std::vector<int>::size_type

你还是不相信使用auto是多么明智的选择?考虑下面的代码:

1
2
3
4
5
6
7
std::unordered_map<std::string, int> m;


for(const std::pair<std::string, int>& p : m)
{
//用p做一些事
}

看起来好像很合情合理的表达,但是这里有一个问题,你看到了吗?

要想看到错误你就得知道std::unordered_mapkeyconst的,所以hash tablestd::unordered_map本质上的东西)中的std::pair的类型不是std::pair<std::string, int>,而是std::pair<const std::string, int>

所以不放直接用auto,避免这些很难被意识到的类型不匹配的错误:

1
2
3
4
for(const auto& p : m)
{
//用p做一些事
}

item6:auto推导若非己愿,使用显式类型初始化惯用法

有些时候auto并不会如你所愿,考虑下面一个例子:

features函数返回一个std::vector<bool>,这里的bool表示Widget是否提供一个独有的特性。

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

假设第5个bit表示Widget是否具有高优先级,我们可以写这样的代码:

1
2
3
4
5
Widget w;

bool highPriority = features(w)[5]; //w高优先级吗?

processWidget(w, highPriority); //根据它的优先级处理w

这段代码没什么问题,work的很好。

如果我们使用auto代替highPriority的显式指定类型做一些看起来很无害的改变:

1
auto highPriority = features(w)[5];     //w高优先级吗?

情况变了。所有代码仍然可编译,但是行为不再可预测:

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

使用autohighPriority不再是bool类型。为什么呢?这里面主要的原因就是vector自身的问题:

虽然从概念上来说std::vector<bool>意味着存放bool,但是std::vector<bool>operator[]不会返回容器中元素的引用(这就是std::vector::operator[]可返回除了bool以外的任何类型),取而代之它返回一个std::vector<bool>::reference的对象(一个嵌套于std::vector<bool>中的类)。

std::vector<bool>::reference之所以存在是因为std::vector<bool>规定了使用一个打包形式(packed form)表示它的bool,每个bool占一个bit。那给std::vectoroperator[]带来了问题,因为std::vector<T>operator[]应当返回一个T&但是C++禁止对bits的引用。无法返回一个bool&std::vector<bool>operator[]返回一个行为类似于bool&的对象。要想成功扮演这个角色,bool&适用的上下文std::vector<bool>::reference也必须一样能适用。std::vector<bool>::reference的特性中,使这个原则可行的特性是一个可以向bool的隐式转化。(不是bool&,是bool。要想完整的解释std::vector<bool>::reference能模拟bool&的行为所使用的一堆技术可能扯得太远了,所以这里简单地说隐式类型转换只是这个大型马赛克的一小块)

对于可以正常运行的代码:

1
bool highPriority = features(w)[5];     //显式的声明highPriority的类型

这里,features返回一个std::vector<bool>对象后再调用operator[]operator[]将会返回一个std::vector<bool>::reference对象,然后再通过隐式转换赋值给bool变量highPriorityhighPriority因此表示的是features返回的std::vector<bool>中的第五个bit,这也正如我们所期待的那样。

然后再对照一下当使用auto时发生了什么:

1
auto highPriority = features(w)[5];     //推导highPriority的类型

同样的,features返回一个std::vector<bool>对象,再调用operator[]operator[]将会返回一个std::vector<bool>::reference对象,但是现在这里有一点变化了,auto推导highPriority的类型为std::vector<bool>::reference,但是highPriority对象没有第五bit的值。

这个值取决于std::vector<bool>::reference的具体实现。其中的一种实现是这样的(std::vector<bool>::reference)对象包含一个指向机器字(word)的指针,然后加上方括号中的偏移实现被引用bit这样的行为。然后再来考虑highPriority初始化表达的意思,注意这里假设std::vector<bool>::reference就是刚提到的实现方式。

调用features将返回一个std::vector<bool>临时对象,这个对象没有名字,为了方便我们的讨论,我这里叫他tempoperator[]temp上调用,它返回的std::vector<bool>::reference包含一个指向存着这些bits的一个数据结构中的一个word的指针(temp管理这些bits),还有相应于第5个bit的偏移。highPriority是这个std::vector<bool>::reference的拷贝,所以highPriority也包含一个指针,指向temp中的这个word,加上相应于第5个bit的偏移。在这个语句结束的时候temp将会被销毁,因为它是一个临时变量。因此highPriority包含一个悬置的(dangling)指针,如果用于processWidget调用中将会造成未定义行为:

1
2
processWidget(w, highPriority);         //未定义行为!
//highPriority包含一个悬置指针!

std::vector<bool>::reference是一个代理类(proxy class)的例子:所谓代理类就是以模仿和增强一些类型的行为为目的而存在的类。很多情况下都会使用代理类,std::vector<bool>::reference展示了对std::vector<bool>使用operator[]来实现引用bit这样的行为。另外,C++标准模板库中的智能指针(见第4章)也是用代理类实现了对原始指针的资源管理行为。代理类的功能已被大家广泛接受。

一些代理类被设计于用以对客户可见。比如std::shared_ptrstd::unique_ptr。其他的代理类则或多或少不可见,比如std::vector<bool>::reference就是不可见代理类的一个例子,还有它在std::bitset的胞弟std::bitset::reference