先观察下面两个例子:
int x;
是不是忘记了给 x 赋初值,因此现在它的值是不确定的。可能应该初始化成 0 吧。还是应该取决上下文吧。Get 到问题了吗?
我们再给局部变量的用迭代器解引用来进行初始化。
template <typename It>
void dwim(It b, It e) //dwim 的算法是迭代 b 至 e 中所有的元素
{
for (; b != e; ++b)
{
typename std::iterator_traits<It>::value_type currValue = *b;
...
}
}
如果想知道迭代器值的类型,你要使用 “typename std::iterator_traits<It>::value_type” 来得到迭代器中值的类型,这非常考验你的记忆能力。
另一个问题就是如果在中声明了一个闭包的局部变量,这个闭包类型只有编译器知道,你是写不出来的。是不是很糟糕。貌似开发 C++ 程序并没有那么快乐。
事实上,早期的 C++ 是解决不了这个问题的,但是到了 C++11,由于有了 auto 加持,类型的推导通过初始化器来得到,所以 auto 变量必须要初始化,这意味着只要你上了现代 C++ 编程的大船,你就不再会遇到变量没有初始化的问题了,来看下面的代码:
int x1; // 未初始化变量
auto x2; // 错误,auto 变量必须初始化
auto x3 = 0; // x3 变量,完美
由于 auto 可以自动推导类型(详见Item2),它可以表示只有编译器知道的类型:
auto derefUPLess = [](const std::unique_ptr<Widget>& p1,
const std::unique_ptr<Widget>& p2)
{
return *p1 < * p2;
};
auto 拿到了 lambda 的类型,看上去是不是非常棒,到了 C++14 这个功能就更炫酷了,因为 lambda 表达式的参数也可以是 auto 变量:
auto derefLess = [](const auto& p1, const auto& p2) // C++14 比较函数,可以比较任何指针指向的值的大小
{
return *p1 < *p2;
};
尽管这样很酷炫,但是你可能觉得闭包不一定要用 auto 来定义类型,因为还可以使用 std::function 对象。当然你有可能你不知道 std::function 是什么东西,下面来讲解一下。
std::function 是 C++11 表中库中的一个通用函数指针的模板。因为函数指针只能指向函数,所以,std::function 可以指向任何一个像函数一样的可调用对象,就像函数指针必须指向指定类型的函数(函数签名相同的函数)一样,std::funcion 必须指向定义类型一样的函数。你可以通过函数模板参数,比如,定义一个 func 的 std::function 对象可以指向任何一个函数签名相同的函数对象。
// C++11 std::unique_ptr<Widget> 比较函数的签名
bool (const std::unique_ptr<Widget>&,
const std::unique_ptr<Widget>&)
你可以这样定义 std::function
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 对象通常会比 auto 对象使用更多的内存空间。同时,由于 std::function 的实现细节使得内联函数使用被限制,导致了间接的函数调用,基本上可以肯定通过 std::function 调用闭包会比使用 auto 慢。换句话说,就是使用 std::function 比 auto 会使用更多的内存并且更慢,而且有可能会抛出内存不足的异常。在看看上面的代码,使用 auto 比 std::function 少敲太多代码了。再对比持有闭包的 std::function 和 auto,auto 更加完美匹配。(另外一个类似的问题,auto 和 std::function 可以持有 std::bind 的调用结果,这个会在 Item34 里讲到,总之,我会说服你在使用 lambda 表达式的时候使用 std::bind)。
auto 的优势在于避免了未初始化的变量,冗长的类型定义,并且可以直接持有一个闭包。还可以避免”类型快捷”的问题。请看下面的代码,你有可能会这么写:
std::vector<int> v;
...
unsigned sz = v.size();
v.size() 表针的返回类型是 std::vector<int>::size_type ,它指定的是一个无符号整型的类型,所以许多程序员认为 unsigned 已经足够来表示上述代码的类型了。这可能会导致一些有趣的结果。在 32 位 windows 下,std::vector<int>::size_type 是 32 位的,与 unsigned 是一样的,但是 64 位 windows,unsigned 是 32 位的,而 std::vector<int>::size_type 是 64 位的。这可能导致代码在 32 位 Windows 上运行正确但是在 64 位 windows 上运行就不正确了,而且如果你同时发布了 32 位 和 64 位 windows 的应用,又有谁会愿意花时间类处理这类问题?
使用 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 的 key 部分是 const,所以 std::pair 在哈希表中的类型不是 std::pair<std::sting, int>,而是 std::pair<const std::string, int>,但是上面的代码漏掉了 const,这导致编译器不得不努力想办法将 std::pair<const std::string, int> 转换成 std::pair<std::string, int>(p 的定义) 对象,这将导致创建一个 p 对应类型的临时对象,并拷贝 m 中的成员到这个临时对象中。在循环中每一次迭代结束,这个临时变量都会被销毁。如果你写了这样的代码,你肯定会很惊讶这样的行为,因为你的初衷只是想引用 m 里迭代器的元素。
这种无意识的错误是可以通过 auto 来避免的,请看下面的代码:
for (const auto& p : m) // 与源代码逻辑一致
{
...
}
这样做不但代码运行高效,而且写起来也非常方便。这样的代码还有一个很吸引人的地方在于如果你如果拿到了 p 的地址,你确定它指向了 m 中的一个元素。假如在上面的代码里你用 auto,那么这个指针其实指向的是一个临时对象,并且这个对象在迭代结束的时候就被销毁了。
在最后两个例子中,你应该写 std::vector<int>::size_type 而不是 unsigned,另一个你应该写成 std::pair<const std::string, int> 而不是 std::pair<std::string, int>,在演示了两个类型的隐式推到后,你应该意识到这不是你想要的。如果使用 auto 作为目标变量的类型,你就不必担心这样的问题,同时你也不需要担心对象没有初始化。
有很多理由让你使用 auto 而不是 explicit 的类型定义,虽然 auto 也有不完美之处。auto 的类型推导是通过初始化表达式来得到的,有些初始化表达式的类型和预期的不同,这些情况在什么时候发生以及如何处理在 Item2 和 Item6 中详细讨论,我现在不会解决这个问题,接下来我会把注意力集中在使用 auto 来替换传统的类型定义的差异上:源代码的可读性。
首先,auto 是一个可选项,并不是规定。如果在你的专业判断下,你觉得你的代码非常的干净,并且易于维护,或者用更好的方式使用 explicit 来定义类型,你可以继续这样使用。但是请记住,C++ 在其他语言具体的类型接口方面并没有突破。其他的静态类型编程语言(比如 C#,D,Scala,Visual Basic)或多或少都有这些功能,更不用说其他的一些静态类型语言了(比如 ML,Haskell,OCaml,F# 等等)。在某种程度上,得益于许多动态类型的语言,比如 Perl,Python,还有 Ruby,很少会去显示的定义变量类型。软件开发社区在类型推断方面有着丰富的经验,而且他们证明了这种技术在创建和维护大型的工业级的代码库上并没有冲突。
一些程序员在使用了 auto 导致无法快速浏览代码来确定变量的类型。但是,IDE 拥有展示变量类型的能力,这样可以减轻这个问题(IDE 类型提示在 Item 4 中有提到),而且,在许多案例中,一个抽象视角的类型和确切的类型定义一样有用。通常情况下,知道一个对象是一个容器,或者是一个计数器,再或者是一个智能指针就足够了,没有必要知道他们的确切类型。再则,如果变量名是精心设计的,那么类型在变量名上就已经得到了。
其实,明确的定义类型名往往会出现一些细小的错误,或者造成一些性能上的问题。再则,auto 类型可以在不同的初始化表达式上自由适配。比如,一个函数定义了 int 型的返回值,但是后来你觉得用 long 型更好,调用代码如果使用 auto,那么在再次编译的时候就帮你自动升级了。但是如果变量显示的声明为 int,你就需要找到并修改所有调用该函数部分的代码。
重点回顾
- auto 变量必须初始化,并且不受类型匹配影响,而这些影响可能会影响到代码的移植性和效率问题。同时,auto 还简化了代码,减少了你敲键盘的量。
- auto 类型的一些陷阱问题在 Item 2 和 Item 6 中有详细阐述。