理解了 auto
的推导原则后,就需要认识到使用 auto
所带来的便利性和缺陷,才能正确的使用它。
认识到使用 auto 的好处’
auto 与初始化
当我们定义一个局部变量时,如果该局部变量没有被初始化,那么它的值就是无法预知的。有的时候会由于未初始化的局部变量而导致程序 BUG。
由于 auto
的推导是依赖于其右值的,也就是说如果没有右值,使用auto
就会出错。基于此特性,我们可以使用 auto 来定义一个变量,以让编译器来帮助我们查看一个局部变量是否已经被初始化。
1 |
|
由于 y
没有右值,这将导致编译失败:
[Error] declaration of ‘auto y’ has no initializer
auto 与函数指针
在不使用 auto
的情况下,使用std::function
方式来调用 lambda:
1 |
|
可以看到,使用std::function
的方式,需要在模板参数里面填入指向函数的参数类型,当函数的参数类型改变后,模板里的参数也得改一次,很是麻烦。
使用auto
来实现:
1 |
|
这样就优雅太多了。
除此之外,auto
实际上执行效率高于std::function
,且不会占用额外内存。
由于auto
是在编译器推导的,并不会占用额外的内存,在执行时也是直接调用该函数体。
但std::function
本身就是一个对象,对象本身就会占用更多的内存,在执行时代码也会先执行std::function
中的代码然后最终才会跳转到函数体执行。
auto 与可移植性
假设有变量 v
其类似是std::vector<int>
,那么获取其大小的类型就是std::vector<int>::size_type
。
在 32 位系统中该值就是 32 位的unsigned int
,在 64 位机中就是 64 位的unsigned int
,为了保证移植性,正确的做法是:
1 | std::vector<int>::size_type sz = v.size(); |
但是上面这个写法,依然要确定模板元素的类型。假设以后需求的变动,使得v
的元素类型为float
。那么所有以上代码都需要将模板类型替换为float
,烦人的体力劳动……
在使用auto
后就是这样了:
1 | auto sz = v.size(); |
可以看到这样写的优点:
- 不用关心模板元素的类型,代码更简洁
- 后期需求变动,就算改变了
v
的元素类型,以上代码都不需要做任何变动
auto 与歧义性
使用auto
能在一些时候避免一些歧义性的坑。
比如使用范围for
从容器中获取一个const
引用可能会引起歧义:
1 | std::unorderd_map<std::string, int> m; |
以上使用范围for
的原意是从容器m
中取出一个元素,并以const
引用的方式访问里面的元素。
但是需要注意的是:**std::unorderd_map
的value_type
类型定义原型是std::pair<const Key, T>
**。
也就是说,对于m
来讲,它的元素类型是std::pair<const std::string, int>
,那么p
就和m
的元素类型不匹配,这就很坑了。
这种情况下,编译器往往会拷贝m
中元素的副本,由p
来进行绑定,最终搞了半天,p
操作的不过就是个副本而已!
使用auto
就可以避免这种坑:
1 | for(const auto& p: m) { |
auto 初始化可能会遇到的坑’
类型的隐式转换
假设要从一个std::vector<bool>
类型容器中获取一个结果,可以像如下的编写方式:
1 |
|
运行的结果是:
ret value is 1
这个代码验证无误,但是为了在获取返回值时更为智能一点,我们将返回类型用auto
进行推导:
1 | auto ret = GetResult()[2]; |
在 MSVC 环境下运行便会报错:
Expression:cannot dereference value-initialized vector
iterator.
接下来我们主动让编译器在编译时报错来查看该auto
推导的类型:
1 |
|
编译时报错如下:
hello.cc:16:32: error: aggregate ‘TypeDisplaystd::_Bit_reference type’ has incomplete type and cannot be defined
按照正常理解,auto
应该推导出bool
类型才是,结果却是std::vector<bool>::reference
。
大部分情况下,std::vector::operator[]
会返回该容器元素的引用,但是由于上面定义的容器元素类型为bool
。
这在底层的表示为,一个bool
占用一个bit
,而c++ 是禁止返回一个位的引用的。
所以实际上bool ret = GetResult()[2]
的操作顺序是:
- 返回
std::vector<bool>::reference
类型 - 取第 3 位的值,隐式转换为
bool
类型
而auto ret = GetResult()[2]
得到的类型确是std::vector<bool>::reference
,实际上这个步骤比想象的复杂:
GetResult()
函数返回的是std::vector<bool>
的副本,也就是一个临时对象tmp
- 接下来
operator[]
由于不能返回位的引用,所以得到的是std::vector<bool>::reference
对象 - 这个对象实际上是指向
bool
的指针,接下来再索引到下标为 2 的地址处,也就指向了第 3 位 ret
得到的就是第 3 位的地址(在上面正确的结果中,如果进行了隐式转换,是将第 3 位的值拷贝了一次,而此处并未拷贝)tmp
临时对象内存被释放,最终std::cout
语句所获取的就是个野指针!
解决方案
解决办法就是明确的限定需要进行一次类型转换,使用static_cast
:
1 |
|
其实在很多有隐式类型转换的位置,显示的使用 cast 是一个很好的习惯。