5.用auto代替显式类型声明

前言

 
通过使用auto技术,我们省下了打字的时间,但可能会带来一些手动声明类型所不会造成的错误或性能问题。作为C++开发者,我们应当尽力掌握auto以期获取正确结果。


问题实例

 
在C++11中未曾出现之前,声明一个变量可能会出现三种令人厌恶的情况:

  1. 忘记初始化
    1
    int x;//x's value depends on the context
  2. 类型名过长
    1
    2
    3
    4
    5
    6
    7
    template<typename It>
    void dwim(It b, It e){
    while (b != e) {
    typename std::iterator_traits<It>::value_type currValue = *b;

    }
    }
  3. 变量类型不确定(闭包),只有编译器知道具体类型

C++11引入的auto技术完美解决了上述三点问题。使用auto声明的变量会从初始化语句中推断类型,因此它们必然会被初始化,并且无需写出繁复的类型名称:

1
2
3
4
5
6
7
8
9
10
11
12
13
auto x = 0;//must be initialized

template<typename It> // as before
void dwim(It b, It e){
while (b != e) {
auto currValue = *b;

}
}

auto derefUPLess = []//only compiler know return closure's type
(const std::unique_ptr<Widget>& p1,const std::unique_ptr<Widget>& p2)
{ return *p1 < *p2; };

C++14将这一特性再次升级,现在lambda表达式内部也可以使用auto了:
1
2
auto derefLess = [](const auto& p1,const auto& p2)
{ return *p1 < *p2; }; // to by anythin


std::function

 
有人认为我们没有必要使用auto来声明一个持有closure的变量,因为我们可以使用std::function对象。前提是你了解function object。

std::function是C++11标准库中的一个模板,其类似于一个通用函数指针。函数只能指向一个函数,但std::function可以指向任何一个可调用对象。当你声明一个函数指针时你必须指出其指向函数的签名,function object亦是如此,其使用大致如下所示:

1
2
3
4
//C++11 signature for std::unique_ptr<Widget> comparison function
bool(const std::unique_ptr<Widget>&, const std::unique_ptr<Widget>&)

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

由于lambda表达式生成了可调用对象,因此closure可以保存在std::function对象之中,因此我们在不使用auto技术的前提下可以将derefUPLess函数声明为:
1
2
3
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; };

即使不考虑语句冗长与类型反复撰写,使用std::function与使用auto也截然不同。一个持有closure由auto声明的变量与闭包具备同样的类型,因此其只需要与闭包一致的内存即可。但持有closure的function对象是一个template的实例化,因此其所占有的内存与签名式相关,但该内存可能并不足以存放closure,那么此时function将会分配heap内存以存储closure。这一特性导致function对象往往比auto对象使用更多的内存。此外,由于限制内联及函数简介调用,由一个function对象触发closure往往会慢于auto对象。

总之,function对象往往占用较多的内存且慢于auto对象,并且可能会产生内存异常。使用function也没有auto简洁与直观。


auto的其他优点

 
auto还有一个好处是可以避免”type shortcuts”,比如你可能看过甚至自己写过这样的程序:

1
2
3
std::vector<int> v;
...
unsigned sz = v.size();//其类型不应为unsigned,这里只是因为开发者偷懒

有人认为unsigned类型与vector::size_type一致,但实际上这是错误的,在64位电脑中vector::size_type为64位,而unsigned只有32位,如果使用auto则可以避免这一问题。

又有实例如下:

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

for (const std::pair<std::string, int>& p : m){
// do something with p
}

这个程序看起来并没有什么不对(如果你没有读过Effective STL或类似书籍的话),但实际上,pair的类型并非pair<std::string,int>,而是pair<const std::string,int>。由于类型不一致,编译器将会生成一个临时对象绑定到p上,并且在每一次循环结束后析构该对象。这不仅仅付出了构造、析构的成本,你所有的操作最终都将作用于临时对象之上,对m中的元素并没有发挥任何作用,但使用auto则可避免这一问题:
1
2
3
for(const auto& p:m){
...
}


auto可读性

 
当然,auto并非尽善尽美,原因在auto作类型推衍的根基在于其初始化表达式,而这可能会带来非你所想的类型推衍结果,在Item2与Item6中介绍了它们的产生原因与消除方案,因此本节不再赘述。接下来要讨论的是auto所带来的可读性问题。

首先我们需要明确的是,auto只是一个可选项而非强制性措施。根据以往的编程经验,代码应当尽可能在简洁的同时做到可维护,但auto并没有破坏这些原则,实际上无论是静态语言(C#,VB等),还是动态语言(Haskell,F#等),类似的功能早已出现,类型推衍与传统编程原则并没有任何冲突。

有些开发者会担心使用auto将导致他们无法快速了解某变量的类型,但实际上IDE会显示出这些变量的类型,并且在许多情况下,我们只需要知道对象的大概类型即可(例如只需要了解其是一个指针、迭代器、容器,而不需要知道具体类型)。一个好的变量名应当能够正确地反映出大致数据类型。

事实上,显式类型说明几乎没有特别明显的优点,它可能会引入微妙的错误,又或者会对效率产生一定影响,甚至二者兼而有之。此外,使用auto完成声明的对象会在其初始化表达式发生改变时自动更改类型,这对重构有着莫大帮助。


总结

  1. 使用auto声明对象意味着必须初始化,并且能够规避类型匹配错误与可移植性、效率低下等问题。在此基础之上,auto还能够简化重构过程,节约大量不必要的类型声明代码。
  2. auto可能存在一些缺陷,这些在Item2与Item6中有所描述。