漫谈C++——谈谈在变量中使用auto

C++学习笔记

Posted by Felix Zhang on September 24, 2020

谈谈在变量中使用auto

用作变量类型推演的关键字auto可能是C++11最著名的特性了,所以这篇文章我长话短说,简单写一下我认为比较重要的使用法则。

怎么用 auto

这一节我会写得尽可能短,因为我默认大家都已经用过auto了。如果有谁没用过的话,有大量的书和文章来替我讲这方面的知识。

我们都是用auto这个关键字来代替一个具体变量的类型定义,让编译器自己去寻找和从它的初始化过程中来推演这个变量的类型。最著名的关于auto的用法恐怕莫过于用它来躲开一堆长长的类型名,比如STL容器的iterator,但是它也可以有其他用处:

std::vector<int> nums;
for(auto iter = std::begin(nums); iter != std::end(nums); iter++) {
  auto& n = *iter;
  n = someFunction();
}

在上边这个例子中,iter就被推演为类型std::vector<int>::iterator,然后n的类型被推演成int&。需要注意的是n被显式地声明为引用类型,否则他就是int类型了。

为什么用 auto

在上边的列子中,一个显而易见的好处就是:auto打起来要比std::vector<int>::iterator短多了。另外,也有可能我们会碰到一些完全未知的类型,比如lambda表达式中。有读者可能会有疑惑,明明auto&int&还要长,为什么那里我也要坚持用auto呢?

除了码字码得少一些意外以外,我使用auto还有另外两个理由。

  1. 上下一致性

在某些地方,如果你希望编译器去自己根据上下文环境确定一个变量的类型而是用了auto,那么你在每个类似的地方都要使用它。使用两个不同的编码习惯可能会让读你代码的人疑惑:为什么你这个地方用了一种风格,其他地方用了另一种风格呢,会不会这里有什么需要注意的地方呢?——其实没有,只是你的坏习惯让代码的可读性变差了而已。

  1. 可维护性

另一个原因是一贯地使用auto会改善代码的可维护性。上边例子中,for循环里所有变量的类型都是由nums推导出来了,nums很明显是一系列数字的集合。但是如果有人突然觉得std::vector在这里并不合适呢?或者更有可能是你的产品觉得这里int太小了,要改为unsigned long呢?

如果你像上边那样写,所有你需要做的事情无非就是把std::vector<int>改为std::array<unsigned long>——其他变量的类型变化就交给编译器和auto来处理好了:iter会自动变为std::array<unsigned long>::iterator,然后n变为unsigned long& 。如果你把n显式声明为int&,想想你之后还要改多少东西吧。

怎么用对auto

给没耐心的读者:

初始化auto,请使用不戴花括号的拷贝赋值操作符=

我知道当你一眼看见auto的时候可能内心会蹦出来好几种可能的用法,但是只有一种是正确的。

  • auto x(5)是对的,但是如果你有,比如说一个类叫SomeClass,然后想要用auto x(SomeClass())来得到一个SomeClass类型的x,那么不好意思,你最后得到的其实是一个函数的声明——这个函数的返回类型是自动推演的——即auto在这里的作用。关于auto在函数中的作用我会在之后的一篇文章中总结。
  • auto x{somgthing},能这么写说明你对C++11的initializer_list有了一定了解了;但是在auto中这么写,你会得到一个错误的类型推演——它会把x的类型推演成initializer_list<SomeType>,其中SomeType就是你写的something。同样的,如果你写成auto x = {something}; 它也会把类型自动推演成initializer_list<SomeType>

所以,只有=会最保险地完成初始化auto这个任务。直接写成auto x = something

在使用auto的时候,给变量和函数起一个好名字就显得极为重要了,因为读你代码的人——很有可能是未来的你——只能通过这个变量名来推断它的类型和作用了,除非你要写很多不必要的注释或者指望读者往上翻好多上下文。比如:auto x = someFunc();只告诉了我们someFunc函数的返回类型和x的类型是一样的,是不是很让人恼火?我们即使靠猜都对x的类型好无头绪。另一方面呢,auto points = calculateScore();是不是好很多,我们会觉得这是一个计算分数的函数,返回的结果也八成是和分数有关的数或类型。

什么时候用 auto

这个问题的答案到此就非常明显了:

如果那个地方的非关键中间变量完全依赖于上下文中其他变量的类型,那么就用auto吧。

你会如何初始化固定类型的变量?

如果什么时候我们想把一个变量的类型显式的固定下来呢?下边有两种方法做这件事:要么显式地声明这个变量的类型,要么显式地使用这个变量的构造器:

std::size_t size{2};	//2是int类型,但是我们想要size_t
auto size = std::size_t{2};	//同上

关于上边这两种用法就以下几个方面有一些争执:

清晰性

上边第一种方法会让代码更清晰,读者第一眼看到的就是这个变量被我们固定下来的类型。有了auto后,读者必须从头到尾把整句话都读完,直到他看到了我们显式调用的构造器类型才能知道这个变量的类型。

另一方面呢,有人会说,我们想把某个变量的类型固定下来是一回事,让读者知道这里把这个变量固定成某个类型了却是另一回事——事实上我们确实可以起一个好名字让读者一下就能把这个变量的类型猜个八九不离十。

除此之外,如果一个显示的C++ cast被调用,比如dynamic_cast<Derived*>(basePtr)这个变量的类型已经被声明在了cast当中并且应该也没人会看不见它吧,这里也是使用auto的一个好理由。

强制变量初始化

auto强制一个变量的初始化过程,这是件好事。我们再也不用担心忘记初始化一个变量,因为我们不初始化编译器就没法编译它。不过鉴于几乎所有的编译器见到没有初始化的变量都会朝你吼个半天(warning),而且各种static analyzer也都把这种问题当作“不会行走的三等功”看待,我觉得这个点可能也没那么重要了。

某些无法拷贝的类型

拷贝构造并不是对所有类型都可行的,特别是某些类型既没有拷贝也没有移动构造函数的情形下,直接用auto进行拷贝初始化可能对它们就不太合适了。它就意味着,你无法用auto来初始化类似的对象。

结语

所以就像上边讨论的结果,哪种方法都不是完美的,这就是为什么我除了“使用auto时应该注意上下一致性”以外不愿意给出一个使用auto的一般性原则。希望这篇文章给你带来了一些帮助和思考。

后边我会出另一篇文章,专门讲auto在函数中的用法。