[译] Item 5: Prefer Auto to Explicit Type Declarations.

看下面的声明,很和谐

1
int x;

等等,擦. 忘记初始化了,所以它的值是不确定的。也许,它可能会被初始化成 0。但是很遗憾这是不确定的。

再看看,解引用一个迭代器来初始化局部变量的例子:

1
2
3
4
5
6
7
8
template <typename It>  // algorithm to dwim ("do what I mean")
void dwim(It b, It e)   // for all elements in range from
{                       // b to e
  while (b != e) {
      typename std::iterator_traits<It>::value_type currValue = *b;
      // ...
  }
}

呃,用 typename std::iterator_traits<It>::value_type 来表示迭代器所指向的值?这样真的没问题嚒?

再来,如果我们想要一个闭包类型的局部变量。哦,好吧。只有编译器才知道这个闭包是什么类型,我们根本无法写出这个类型。

擦,擦,擦。C++ 写起来还真是头疼。没错,不过那都是过去了。有了 C++11 之后,这些问题都不存在了,我们有了 autoauto 类型会根据初始化自动推导,所以它们必须被初始化。这意味着在现代 C++ 中你可以和那些变量未初始化的问题挥手说拜拜了:

1
2
3
int x1;        // potentially uninitialized
auto x2;       // error! initializer required
auto x3 = 0;   // fine, x's valye is well-defined

迭代器解引用初始化局部变量也可以这么来写了:

1
2
3
4
5
6
7
8
template <typename It>  // as before
void dwim(It b, It e)
{
  while (b != e) {
      auto currValue = *b;
      // ...
  }
}

由于 auto 类型推导(Item 2),它也可以表示那些只有编译器才知道的类型了:

1
2
3
4
auto derefUPLess =                         // comparison func.
  [](const std::unique_ptr<Widget>& p1,    // for Widgets
     const std::unique_ptr<Widget>& p2)    // pointed to by
  { return *p1 < *p2; };                   // std::unique_ptrs

很酷吧。C++14 更牛逼了,lambda 可以用 auto 类型的参数:

1
2
3
4
auto derefUPLess =          // C++14 comparison
  [](const auto& p1,        // function for
     const auto& p2)        // values pointed
  { return *p1 < *p2; };    // to by anything pointer-like

尽管很酷,但你也许会想我们不需要使用 auto 来声明一个闭包,我们有 std::function。是的,可以,但是这也许会和你想的不太一样。现在你可能会想 std::function 对象到底是什么?接下来我们把这个问题讨论清楚。

std::function 是 C++11 标准库中的模板类,是函数指针的升级版。函数指针只能指向函数,而 std::function 对象可以表示任何可调用的对象,即任何可以像函数一样调用的对象。就像你必须给函数指针声明一个明确类型一样(类型签名必须与想要调用的函数类型一致),你必须明确 std::function 对象所涉及到的类型。也就是 std::function 的模板参数。举个例子,你想要声明一个 std::function 对象 func 它可以调用如下签名的函数:

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

你需要这么写:

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

由于 lambda 表达式生成可一个可调用的对象,那么闭包可以存在一个 std::function 对象中。也就是说在 C++11 中, 不使用 auto 我们可以像下面这样:

1
2
3
4
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::functionauto 也不完全一样。用 auto 类型来接受一个闭包,它的类型与实际类型是一致的,内存的占用也是完全一样的。而用 std::function 来接受一个闭包,那么对于给定的签名,内存占用是固定的。而这个大小可能不足以存储该闭包,这个时候 std::function 的构造函数会在堆上分配足够的内存来存储这个闭包。也就是说,通常情况下 std::function 对象会比 auto 对象使用更多的内存。同时会阻止函数内连,让函数调用多一个间接层,通过 std::function 来调用一个闭包集合总是要比 auto 声明的闭包要慢。换句话说,std::function 通常都会比 auto 更大,更慢,而且还可能抛出 out-of-memory 异常。另外,像上面的例子,auto 会比完整类型更简洁。总之,当需要持有一个闭包时,auto 是比 std::function 更好的选择。(另外还有一个类似的东西是 std::bind,同样的也是选择使用 auto 而不是 std::function,不过,在 Item 34 中,我会尽力说服你用 lambda 表达式来代替 std::bind)。

auto 除了能避免未初始化变量,显式声明,直接保存闭包。另一个时可以避免 “短类型”,下面是你曾经可能见到过的代码:

1
2
3
std::vector<int> v;
// ...
usigned sz = v.size();

v.size() 的正确类型是 std::vector<int>::size_type,但是很少有程序员意识到它。std::vector<int>::size_type 内部实现的确是无符号类型,因此很多程序员使用 unsigned 写出了上面的代码。这会产生有趣的结果。在 32 位 Windows 上,unsignedstd::vector<int>::size_type 类型完全一致,但是在 64 位 Windows 上,unsigned 是 32 位,而 std::vector<int>::size_type 却是 64 位。这意味着那些在 32 位机器上运行正常的代码可能在 64 位机器上产生错误,另外当将你的程序从 32 位移植到 64 位机器上时,谁愿意花时间在这些问题上呢?

如果使用 auto 就可以不必理会这个问题了:

1
auto sz = v.size() // sz's type is std::vector<int>::size_type

还不确定 auto 够不够好嚒?考虑下面的代码:

1
2
3
4
5
std::unorderd_map<std::string, int> m;
// ...
for (const std::pair<std::string, int>& p : m) {
  // ...    do something with p
}

看起来很完美?但是这是有问题的,你看发现了吗?

要意识到 std::unorderd_map 的 key 类型是 const,因此散列表(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> 对象。这会拷贝 m 的每个元素,然后将临时对象绑定到 p。在每次循环迭代的最后临时对象会释放。如果你写了上面的代码,你一定会被上面的行为惊到,因为你的意图仅仅是将 p 引用到 m 的每个元素上而已。

像这样的误用,我们也可以用 auto 解决:

1
2
3
for (const auto& p : m) {
  // ...  as before
}

这不仅是高效的,而且写起来也更方便。还不仅如此,如果你想要取 p 的地址,你会取到 m 中的元素的指针。在没有使用 auto 的代码中,你取到的是临时对象的指针 —— 它会在当此循环结束时销毁。

最后两个例子 —— 使用 unsinged 代替 std::vector<int>::size_type 以及 std::pair<std::string, int> 代替 std::pair<const std::string, int> —— 展示了明确类型会导致的那些你不想要的隐式转换。如果你使用 auto 就不用担心声明的类型与表达式实际类型不一致的问题了。

还有很多使用 auto 类型的理由。虽然 auto 是不完美的。auto 类型是根据初始化的表达式类型自动推导的,而有时候自动退到出的类型不是我们期待的类型。这种情况我们在 Item 2 与 6 中讨论。在这里,我们把注意力转移到另外一个问题上,你可能会有用 auto 代替传统的类型声明的问题:源代码可读性问题。

首先,做一次深呼吸,放松一下。auto 只是一种选择,而不是强制的任务。如果在你有专业的判断,使用显式类型的声会更清晰和更容易维护或以某种其他方式更好地通过,你可以自由地继续使用它们。C++ 没有采用什么新的东西,而只是运用早已被大家所熟知的类型推断而已。其他静态语言 (比如 C#, D, Scala, Visual Basic) 或多或少的都包含这种特性,更不用说静态类型的函数式语言(比如 ML, Haskell, OCaml, F# 等)了。这也归功于那些几乎从不明确类型的动态语言 Perl, Python,Ruby 的成功。软件开发社区在使用类型推断方面有着丰富的经验,它显示了这种技术与创建和维护大型、 工业强度的代码是有没有矛盾的。

一些开发者可能会由于无法在阅读代码时第一时间知道对象类型而感到不安。不过,IDE 通常都有办法展示出对象的类型来缓解这个问题(Item 4 中我们有提到这个问题),并且在通常情况下一个抽象的类型就可以与一个明确类型一样提供给我们足够的信息。比如,知道一个对象是容器或者计数器或者一个智能指针,而不知道它们的具体类型。如果我们精心挑选了有意义的变量名,那么这些抽象类型的信息就很容易知道了。

事实就是写明确类型往往会引入一些小错误,无论是类型的正确性,还是效率方面。此外,auto 类型在你更改了初始化表达式的时候会自动更改,这意味着你重构代码时一些代码的重构由 auto 代为处理了。举个例子,现在有一个函数的返回值是 int 类型,但不久之后,你发现 long 是个更好的选择,那么哪些用 auto 来接收函数返回值的地方在你下一次编译的时候会自动更新。如果那些代码是明确用 int 类型接收的话,你就需要找到所有那些调用的地方,一个个的更改它们。

需要记住的

  • auto 变量必须被初始化,它可以有效避免类型不匹配造成的移植性问题与性能问题,可以方便重构,通常也会比明确类型打更少的字。
  • auto 类型的陷阱在 Item 2 与 Item 6 中。

Comments