Effective Modern C++翻译(6)-条款5:auto比显示的类型声明要更好

时间:2022-05-04 15:02:59

    在概念上说,auto关键字和它看起来一样简单,但是事实上,它要更微妙一些的。使用auto会让你在声明变量时省略掉类型,同时也会防止了手动类型声明带来的正确性和性能上的困扰;虽然按照语言预先定义的规则,一些auto类型推导的结果,在程序员的视角来看却是难以接受的,在这种情况下,知道auto是如何推导答案便是非常重要的事情,因为在所有可选方法中,你可能会回归到手动的类型声明上(because falling back on manual type declarations is an alternative that’s often best avoided.)。

这一章包括了auto的细则

 

条款5:auto比显示的类型声明要更好

这是一个看起来非常简单的例子

int x;

等一下,该死,我忘记初始化x了,所以它的值是不确定的,也许它被初始化为0了,不过这要取决于它的上下文,哎…

没有关系,接下来,在下面这个例子中我们通过解引用一个迭代器来初始化一个变量currValue

template<typename It> //函数的名字是dwim ("do what I mean")
void dwim(It b, It e) // 对于所有b到a
{ // 范围内的元素
while (b != e) {
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; // error!需要初始化

auto x3 = 0; //很好,x3的值已经被初始化

 

你同样可以通过使用auto来声明一个局部变量,用解引用一个迭代器来初始化对应的值。

template<typename It> // 和之前一样

void dwim(It b, It e)
{
while (b != e) {
auto currValue = *b;

}
}

因为auto使用了类型推导规则(参见条款2),它可以表示一些只有编译器知道的类型

auto derefUPLess = // 一个比较函数.
[](const std::unique_ptr<Widget>& p1, //std::uniquer_ptr
const std::unique_ptr<Widget>& p2) //的widget
{ return *p1 < *p2; }; //对象

很酷吧,在C++14中,auto又前进了一步,因为lambda表达式的参数也可以使用auto了

auto derefLess =      // C++14中
[](const auto& p1, //的比较函数
const auto& p2) // 可以比较任何
{ 
    return *p1 < *p2; //指针指向的 
};                    // 东西
 

也行你认为我们不需要使用auto来声明持有一个闭包(holds a closure)的变量,因为我们可以使用std::function对象来完成同样的操作,是的,的确可以,但,恩,你也许要问了,std::function这又是什么东西?

 

std::function是C++11标准库中用来模拟函数指针的东西,函数指针只能指向函数,类似的std::function只能指向任何的可调用对象(callable object),可调用对象是被认为可以像函数一样的东西,就像你声明函数指针的时候,你必须标注出函数的类型,当你声明std::function的时候,你也必须通过模板的参数标注出函数的类型,例如你可以声明一个叫func的std::function对象,它可以指向以下函数类型的可调用对象:

bool(const std::unique_ptr<Widget>&, // C++11中比较
const std::unique_ptr<Widget>&) // std::unique_ptr<Widget>
//的函数原型

完整的结果是这样的:

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

 

因为lambda表达式产生的可调用对象,闭包也可以通过std::function对象表示,这意味着我们可以声明新的版本的derefUPLess而不需要使用auto关键字

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调用一个闭包也更慢一些,简而言之就是std::function比auto声明的闭包要更大,更慢一些,加上它可能产生内存不足的异常,你也看到了上面的那个例子,使用auto会比使用std::function达成同样的效果轻松很多,所以在使用auto还是std::function声明一个闭包的较量中,auto获胜了(一个类似的参数可以通过auto或者std::function来产生,持有std::bind的调用结果,但是根据条款34,,我会尽我最大的努力来让你使用lambdas替代std::bind)(

A similar argument can be

made for auto over std::function for holding the result of calls to std::bind, but

in Item 34, I do my best to convince you to use lambdas instead of std::bind, anyway.

)

auto的优势除了避免忘记初始化变量,冗长的变量声明式,持有闭包的能力之外,另一个可以避免的问题是类型截断,这是一个你之前可能见到过的例子。

std::vector<int> v;

unsigned sz = v.size();

v.size()的返回类型是std::vector<int>::size_type,但是很少会有程序员意识到std::vector<int> ::size被指定为一个无符号整形了,很多程序员认为unsigned就可以了,写出了如上的程序代码,这可能会引起很多有意思的后续反应,在32位windows机器上,unsigned和std::vector<int>::size_type的类型是一样的,但是在64位机器上,unsigned是32位,然而std::vector<int>::size_type是64位,这意味你的程序可能在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::unorder_map的key是const,所以std::pair的类型不应是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可以避免这样意料之外的事情。

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而不是显示的类型声明就有很多的理由了,是的,auto也并不完美,auto声明的变量的类型会从相应的初始化式中推导出来,一些推导的结果可能不是你所期待或想要的,在某些情况下,你需要了解条款2和6中讨论过的问题,所以我不会在这个再强调这个问题了,相反,我会把我的精力转到另一个方面,auto代码具有更好的可读性。

 

先放松一下吧,auto也只是可选的,并不是强制的,如果在你的判断中,使用显示的类型声明会让你的代码更整洁,并且更容易可维护的话,你可以继续使用它,但是要记住,C++并没有创造出一个新的东西,这个东西在编程界已经存在了,被叫做类型推导,一些其他的静态类型的语言(比如C#,D,Scala,Visual Basic)也或多或少有了类似的特性,更不用说那些静态类型的函数式语言了(比如ML,Haskell,OCaml,F#)在某种程度上,这是由于动态声明的语言例如Perl,python或者是Ruby,这些语言几乎不使用显示的类型声明的广泛发展,软件社区在类型推导上有了很多的经验,证明这些技术可以被应用在传统的大型的可创造新,可维护性的优质的代码库上。

 

有些开发者可能认为使用auto时,会让你难以在第一时间看出变量的类型是什么,然而IDE本身显示变量类型的能力可以减轻这个问题(可以参考条款4中讨论的IDE展示问题),而且在很多情况下抽象的变量类型会和精确的类型一样有效,例如,只是为了知道一个对象是容器,计数器,智能指针,而不关注这个容器,计算器或者智能指针的精确类型是什么,此外如果你的变量的名字起的足够好的话,知道变量的抽象类型是件很容易的事情。

 

事实是显示的类型声明会引入一些微小的错误,此外使用auto初始化的变量的类型会随着初始化式类型的变化自动发生变化,这同时意味着在代码利用auto,会让重构变的简单,例如,如果一个函数最初的返回值是int,但是后来你觉得long更好,如果你使用auto储存函数的返回类型的话,代码会自动下一次编译的时候自动更新,但是你使用了显示的类型声明int,你可能需要修改每一个函数调用的地方。

 

请记住

  • 1、使用auto声明的变量必须被初始化,这不会导致类型不匹配照成的可移植性和效率问题,可以减轻重构的过程,并且通常比显示的类型声明需要更少的代码。
  • 2、auto使用的一些陷阱在条款2和条款6中描述了。