用auto声明的变量,其型别都推导自其初始化物,所以它们必须初始化。所以,当你在现代C++的高速公路上飞驰时,可以向那一系列由未初始化的变量带来的问题挥手告别了:
int x1; // 有潜在的未初始化风险
auto x2; // 编译错误,必须有初始化物
auto x3 = 0; // 没问题,x的值有着合适定义
这条高速公路上可没有使用提领迭代器来声明局部变量时会遇到的那些坑:
template<typename It>
void dwim(It b, It e)
{
while (b != e)
{
auto currValue = *b;
...
}
}
而且,由于auto使用了型别推导,就可以用它来表示只有编译器才掌握的型别:
auto derefUPLess =
[](const std::unique_ptr<Widget>& p1,
const std::unique_ptr<Widget>& p2)
{
return *p1 < *p2
}
这已经够酷了,但在C++14中,更是酷毙了,因为连lambda表达式的形参中都可以使用auto:
auto derefLess =
[](const auto& p1, //可以应用于任何类似指针之物
const auto& p2) //指涉到的值
{ return *p1 < *p2; };
撇开这样写有多酷不谈,也许你会感觉我们并不需要声明变量来持有闭包,因为我们可以使用std::function对象来完成这件事。这种说法是成立的,但是结果未必如你所想。好,你现在可能就在想,“啥是std::function对象啊?”那我们就先来搞清楚这个问题。
std::function是C++11标准库中的一个模板,它把函数指针的思想加以推广。函数指针只能指涉到函数,而std::function却可以指涉任何可调用对象,即任何可以像函数一样实施调用之物。正如你若要创建一个函数指针就必须指定欲指涉到的函数的型别(即该指针指涉到的函数的签名),你若要创建一个std::function对象就必须指定欲指涉的函数的型别。这一步是通过std::function模板形参来完成的。举个例子,声明一个名为func的std::function对象,它可以指涉到任何能够以下述签名调用的对象。
bool(const std::unique_ptr<Widget>&, //C++11版本的
const std::unique_ptr<Widget>&) //std::unique_ptr<Widget>
//比较函数签名
这样来定义func:
std::function<bool(const std::unique_ptr<Widget>&,
const std::unique_ptr<Widget>&)> func;
因为lambda表达式可以产生可调物对象,std::function对象中就可以存储闭包。这就意味着,在C++11中,不用auto也可以声明derefUPLess如下:
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还是有所不同。使用auto声明的、存储着一个闭包的变量和该闭包是同一型别,从而它要求的内存量也和该闭包一样。而使用std::function声明的、存储着一个闭包的变量是std::function的一个实例,所以不管给定的签名如何,它都占有固定尺寸的内存,而这个尺寸对于其存储的闭包而言并不一定能够用,如果这样的话,std::function的构造函数就会分配堆上的内存来存储该闭包。从结果上看,std::function对象一般都会比使用auto声明的变量使用更多内存。再有编译器的实现细节一般都会限制内联。并会产生间接函数调用,把这些因素考虑在内的话,通过std::function来调用闭包几乎必然会比通过使用auto声明的变量来调用同一闭包来的慢。换言之,std::function手法通常比起auto手法来又大又慢,还可能导致内存消耗异常。还有,在前述例子中,写一个“auto”可比写一整个std::function实例型别要省事多了。在持有闭包的这场发生在auto和std::function之间的较量中,auto可谓大获全胜(如果再来一场较量,在auto和std::function之间比较持有std::bind的调用结果,则比分是一样的)。
auto的优点还不止这些,除了避免未初始化的变量和啰嗦的变量声明,并且可以直接持有闭包外,它还可以避免一类我称为“型别捷径”的问题。下面的代码你可能曾经看到过,甚至曾经写过:
std::vector<int> v;
unsigned sz = v.size();
标准规定,v.size()的返回值型别应为std::vector<int>::size_type,但只有很少一部分程序员会注意到,std::vector<int>::size_type仅仅规定成一个无符号型整形,所以很多程序员感觉unsiged足够了,于是就这样写代码。但会导致一些有意思的后果。比如说,在32位Windows上,unsigned和std::vector<int>::size_type的尺寸是一样的,但在64位windows上,unsigned是32位,而std::vector<int>::size_type则是64位。这就意味着,在32位Windows上运行正常的代码可能在64位Windows上会表现异常,在将你的应用从32位移植到64位时,谁会愿意花费时间在解决这样的问题上呢?
而使用auto就可以保证你不会这样浪费时间:
auto sz = v.size(); //sz的型别是std::vector<int>::size_type
还是感觉对auto的大智慧有点拿不准?那就看看下面这段代码:
std::unordered_map<std::string, int> m;
...
for (const std::pair<std::string, int>& p : m)
{
... //在p上实施某些操作
}
这段代码看起来完全合情合理,但其中暗藏隐患。你看出来了没有?
要想意识到缺失的是什么,就要记住std::unordered_map的键值部分是const,所以哈希表中的std::pair(也就是std::unordered_map本身)的型别并不是std::pair<std::string, int>,而是std::pair<const std:::string, int>。可是,在上面的循环中,用以声明变量p的型别却并不是这个。所以编译器就要开足马力找到某种办法把std::pair<const std::string, int>对象(即哈希表中的元素)转换成std::pair<std::string, int>对象(声明p的型别)。这一步是可以成功的,方法是对m中的每个对象都做一次复制操作,形成一个p想要绑定的型别的临时对象,然后把p这个引用绑定到该临时对象。在循环每次迭代结束时,该临时对象都会被析构一次,如果这个循环是你写的,你恐怕会惊异于其表现,因为你想要的效果几乎肯定只是想把引用p依次绑定到m中的每个元素而已。
这样的无心之错引发的型别不匹配可以轻松地使用auto化解:
for (const auto& p : m)
{
...
}
这样做不仅运行效率能提升,而且打字也更少。犹有进者,这段代码还有一个极其诱人的特点,那就是如果对p取地址,肯定会取得一个指涉到m中的某个元素的指针。而在那段没有使用auto的代码中,取得的则是一个指涉到临时对象的指针,并且这个对象会子啊循环迭代结束时被析构。
显示指定型别可能导致你既不想要,也没想到的隐式型别转换。如果你使用auto作为目标变量的型别,就完全没必要担心在用以声明变量的型别和它的初始化表达式的型别之间发生的不匹配了。
事实上,显示地写出型别经常是画蛇添足,带来各种微妙的偏差,有些关乎正确性,有些关乎效率,或者两者都受影响。还有,auto型别可以随着其初始化表达式的型别变化而自动随之改变,这就意味着通过使用auto,有一些重构动作就被顺手做掉了。例如,假设有一个函数本来声明的函数返回型别是int,但后来又觉得long更合适一些,那么如果函数调用的结果是存储在auto变量中的,则调用它的代码在下一次编译时就会自动更新自己。但如果调用的结果是存储在声明为int的变量中的话,就需要找到这个函数的所有调用点,才能更新它们了。