前言
通过使用auto技术,我们省下了打字的时间,但可能会带来一些手动声明类型所不会造成的错误或性能问题。作为C++开发者,我们应当尽力掌握auto以期获取正确结果。
问题实例
在C++11中未曾出现之前,声明一个变量可能会出现三种令人厌恶的情况:
- 忘记初始化
1
int x;//x's value depends on the context
- 类型名过长
1
2
3
4
5
6
7template<typename It>
void dwim(It b, It e){
while (b != e) {
typename std::iterator_traits<It>::value_type currValue = *b;
…
}
} - 变量类型不确定(闭包),只有编译器知道具体类型
C++11引入的auto技术完美解决了上述三点问题。使用auto声明的变量会从初始化语句中推断类型,因此它们必然会被初始化,并且无需写出繁复的类型名称:1
2
3
4
5
6
7
8
9
10
11
12
13auto 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
2auto 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
3std::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
3std::vector<int> v;
...
unsigned sz = v.size();//其类型不应为unsigned,这里只是因为开发者偷懒
有人认为unsigned类型与vector
又有实例如下:1
2
3
4
5std::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
3for(const auto& p:m){
...
}
auto可读性
当然,auto并非尽善尽美,原因在auto作类型推衍的根基在于其初始化表达式,而这可能会带来非你所想的类型推衍结果,在Item2与Item6中介绍了它们的产生原因与消除方案,因此本节不再赘述。接下来要讨论的是auto所带来的可读性问题。
首先我们需要明确的是,auto只是一个可选项而非强制性措施。根据以往的编程经验,代码应当尽可能在简洁的同时做到可维护,但auto并没有破坏这些原则,实际上无论是静态语言(C#,VB等),还是动态语言(Haskell,F#等),类似的功能早已出现,类型推衍与传统编程原则并没有任何冲突。
有些开发者会担心使用auto将导致他们无法快速了解某变量的类型,但实际上IDE会显示出这些变量的类型,并且在许多情况下,我们只需要知道对象的大概类型即可(例如只需要了解其是一个指针、迭代器、容器,而不需要知道具体类型)。一个好的变量名应当能够正确地反映出大致数据类型。
事实上,显式类型说明几乎没有特别明显的优点,它可能会引入微妙的错误,又或者会对效率产生一定影响,甚至二者兼而有之。此外,使用auto完成声明的对象会在其初始化表达式发生改变时自动更改类型,这对重构有着莫大帮助。
总结
- 使用auto声明对象意味着必须初始化,并且能够规避类型匹配错误与可移植性、效率低下等问题。在此基础之上,auto还能够简化重构过程,节约大量不必要的类型声明代码。
- auto可能存在一些缺陷,这些在Item2与Item6中有所描述。