《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++禁止对bit
s的引用。无法返回一个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 协议 ,转载请注明出处!