《EffectiveModernC++》第二章:auto
第二章 auto
item5:优先考虑auto而非显式类型声明
1 | |
typename std::iterator_traits<It>::value_type是想表达迭代器指向的元素的值的类型,然而我们即使写出 typename std::iterator_traits<It>::value_type也不知道他具体是一个什么类型,只不过是萃取器帮我们找到了对应的valuetype,此时不妨直接使用auto。
1 | |
很酷对吧,如果使用C++14,将会变得更酷,因为lambda表达式中的形参也可以使用auto:
1 | |
但是你可能会想我们完全不需要使用auto声明局部变量来保存一个闭包,因为我们可以使用std::function对象。std::function对象到底是什么?
std::function是一个C++11标准模板库中的一个模板,它泛化了函数指针的概念。与函数指针只能指向函数不同,std::function可以指向任何可调用对象,也就是那些像函数一样能进行调用的东西。当你声明函数指针时你必须指定函数类型(即函数签名),同样当你创建std::function对象时你也需要提供函数签名,由于它是一个模板所以你需要在它的模板参数里面提供。举个例子,假设你想声明一个std::function对象func使它指向一个可调用对象,比如一个具有这样函数签名的函数,
1 | |
你就得这么写:
1 | |
因为lambda表达式能产生一个可调用对象,所以我们现在可以把闭包存放到std::function对象中。这意味着我们可以不使用auto写出C++11版的derefUPLess:
1 | |
一个持有闭包(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 | |
v.size()的标准返回类型是std::vector<int>::size_type,但是只有少数开发者意识到这点。std::vector<int>::size_type实际上被指定为无符号整型,所以很多人都认为用unsigned就足够了,写下了上述的代码。这会造成一些有趣的结果。举个例子,在Windows 32-bit上std::vector<int>::size_type和unsigned是一样的大小,但是在Windows 64-bit上std::vector<int>::size_type是64位,unsigned是32位。这意味着这段代码在Windows 32-bit上正常工作,但是当把应用程序移植到Windows 64-bit上时就可能会出现一些问题。谁愿意花时间处理这些细枝末节的问题呢?
所以使用auto可以确保你不需要浪费时间:
1 | |
你还是不相信使用auto是多么明智的选择?考虑下面的代码:
1 | |
看起来好像很合情合理的表达,但是这里有一个问题,你看到了吗?
要想看到错误你就得知道std::unordered_map的key是const的,所以hash table(std::unordered_map本质上的东西)中的std::pair的类型不是std::pair<std::string, int>,而是std::pair<const std::string, int>。
所以不放直接用auto,避免这些很难被意识到的类型不匹配的错误:
1 | |
item6:auto推导若非己愿,使用显式类型初始化惯用法
有些时候auto并不会如你所愿,考虑下面一个例子:
features函数返回一个std::vector<bool>,这里的bool表示Widget是否提供一个独有的特性。
1 | |
假设第5个bit表示Widget是否具有高优先级,我们可以写这样的代码:
1 | |
这段代码没什么问题,work的很好。
如果我们使用auto代替highPriority的显式指定类型做一些看起来很无害的改变:
1 | |
情况变了。所有代码仍然可编译,但是行为不再可预测:
1 | |
使用auto后highPriority不再是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::vector的operator[]带来了问题,因为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 | |
这里,features返回一个std::vector<bool>对象后再调用operator[],operator[]将会返回一个std::vector<bool>::reference对象,然后再通过隐式转换赋值给bool变量highPriority。highPriority因此表示的是features返回的std::vector<bool>中的第五个bit,这也正如我们所期待的那样。
然后再对照一下当使用auto时发生了什么:
1 | |
同样的,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>临时对象,这个对象没有名字,为了方便我们的讨论,我这里叫他temp。operator[]在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 | |
std::vector<bool>::reference是一个代理类(proxy class)的例子:所谓代理类就是以模仿和增强一些类型的行为为目的而存在的类。很多情况下都会使用代理类,std::vector<bool>::reference展示了对std::vector<bool>使用operator[]来实现引用bit这样的行为。另外,C++标准模板库中的智能指针(见第4章)也是用代理类实现了对原始指针的资源管理行为。代理类的功能已被大家广泛接受。
一些代理类被设计于用以对客户可见。比如std::shared_ptr和std::unique_ptr。其他的代理类则或多或少不可见,比如std::vector<bool>::reference就是不可见代理类的一个例子,还有它在std::bitset的胞弟std::bitset::reference。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!