Effective Modern C++: Item 5 -> 优先选择auto而不是显式类型声明

来源:互联网 发布:商之翼yii2源码 编辑:程序博客网 时间:2024/06/04 23:29

概念上讲,auto真的特别简单,但是它实际上比它看起来要更加巧妙。使用它确实可以节省打字时间,但是它又是也会导致一些问题,会对手动类型声明造成一定的困扰。另外,有一些auto的类型推断结果,尽管是按照特定的算法执行的,但是从程序员的角度来看,它就是错的。当你真遇到这个问题时,很重要的一点就是将auto改对,使得其得出正确的答案,而最好不是自己手动输入类型声明。

优先选择auto而不是显示类型声明

哈,小试牛刀一下

int x;

等一下。Damn!我忘记初始化x了,所以它的值现在是不确定的。或许,它实际上被初始化成0了,取决于它的上下文。哎!

没关系。我们再小试牛刀一下,声明一个局部变量,通过解引用一个迭代器来初始化:

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”来表示迭代器所指向的值的类型?真的吗?我一定忘了这其中有多少乐趣。Damn!等一下–我真的没有说过这个?

好吧,简单的快乐:声明一个类型跟一个闭包(closure)类型一样的局部变量的乐趣。嗯。这个闭包的类型只有编译器知道,所以没法写出来。哎,Damn!

Damn,damn,damn!C++编程并没有认为的的那么爽!

其实则个不是一直都这样的。但是到了C++11,所有这些问题都没啦,多亏了autoauto变量根据它们的初始值来推断它们的类型,所以它们必须被初始化。那也就意味着一旦你上了现代C++的高速公路,你就可以和一大堆未初始化变量问题说拜拜了:

int x1; //potentially uninitializedauto x2; //error!initializer requiredauto x3 = 0;//fine, x's value is well-defined

刚说的高速公路上还可以声明解引用迭代器类型的变量:

template<typename It>   //as beforevoid dwim(It b, It e) {    while(b != e){        auto curValue = *b;        ...    }}

由于auto可以使用类型推断(见Item 2),它可以表示只有编译器知道的类型:

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

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

尽管很酷,或许你在想我们其实并不需要auto来声明一个闭包变量,因为我们可以使用std::function对象。的确,我们是可以,但是或许这并不是你刚刚在想的。也许你现在正在想:”什么是std::function对象?”现在咱们开始了解。

std::function是C++11标准库中的一个模板,可以具化成任意的函数指针。然而函数指针只能指向函数,而std::function可以指任何可调用对象,也就是说,任何可以像函数那样被调用的对象。正如你在创建函数指针的时候必须指定其指向的函数的类型(即你想指向的函数签名),你在创建std::function对象的时候也必须指定其所指的函数类型。你可以通过std::function的模板参数来完成。例如,为了声明一个std::function对象func,使其可以指向任何可调用对象,就像它拥有以下签名一样:

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

你可能会这样写:

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

因为lambda表达式就是可调用对象,所以其闭包可以存储在std::function对象里面。那意味着我们可以声明C++11版本的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声明的变量要大。并且,多亏了其内部实现细节,诸如限制了inline并且是间接函数调用,所以通过std::function来调用一个闭包几乎肯定要比通过auto声明的对象调用要慢。换句话说,std:function这种方法总体来说要比auto这种方法要更慢更占空间,同时也可能造成内存不足等异常。还有,正如你在上面例子里看到的,写auto可比写std::function类型的实例要省力多了。在autostd::function包含一个闭包的竞赛中,auto可谓大获全胜。

auto的优势已经不止是避免未初始化变量,避免冗余的变量声明,以及直接包含闭包的能力,另外还有一种避免我称之为type shortcut相关问题的能力。下面是一些你可能见过的代码–或许你还曾经写过:

std::vector<int> v;...unsigned sz = v.size();

v.size()的官方返回类型是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可以保证你不需要这么做:

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

还是不确定使用auto的智慧?那么考虑如下代码:

std::unordered_map<std::string,int> m;...for(const std::pair<std::string,int>& p:m){    ...   //do something with p}

这看起来无比合理,但是却存在一个问题。你能看出来吗?

看出来什么缺失需要记得一个std::unordered_map的关键部分是const,所以在哈希表(也就是这个std::unordered_map)中的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>对象(p的声明类型)。编译器们会通过创造一个临时对象来保存从m中拷贝出来的相应元素,该临时对象的类型就是p想要绑定的类型。在每次循环迭代结束后,临时对象都会被销毁。如果你写了这个循环,你将很可能会被其行为惊讶到,因为你本来肯定是想将引用p绑定到m的每一个元素上的。

这样的无心的类型错误可以被auto修正:

for(const auto& p: m)  //译者注:这里加不加const都无所谓的{    ...   //as before}

这不仅更加高效,而且书写也更简单。另外,上面的代码还有一个迷人的特性,那就是如果你使用p的地址,那么你肯定得到一个指向m内部元素的指针。但在上面那个没有使用auto的代码中,你得到的是一个临时对象的指针–一个在每次循环迭代之后会被销毁的对象。

这最后两个例子–在你本该用auto却使用了unsigned以及在你本该使用auto却使用了std::pair<std::string,int>–展示了显示指定类型是如何导致你不想也不期望的隐性转换。如果你使用auto作为你的目标变量的类型,你就不必担心你声明的变量和用来初始化它的表达式的类型之间的不匹配问题。

所以现在有了好几个理由,让你优先选择auto而不是显示类型声明。但是auto也不是完美的。auto的类型是根据他的初始化表达式来确定的,但是有一些初始化表达式的类型既不是预期的也不是你想要的。什么情况下这种事情会发生,以及你该怎么对付它们,分别在Item 2和Item 6中进行了讨论,所以这里不再赘述。相反地,我将转移我的注意力到一个你可能也有过的顾虑,就是使用auto取代了传统的类型声明:其导致的源代码可读性问题。

第一,深吸一口气并放松。auto只是一个选择,不是一个必须。如果在你专业的判断下,你的代码使用显示类型声明会变得更加简洁或者可维护性更好亦或者其他方面更好,那么欢迎你继续使用它们。但是请记住,C++在被编程语言世界普遍称为类型推断的领域并没有什么新的突破。一些其他的静态语言程序语言(比如C#,D,Scala,Visual Basic)都或多或少有一些等价的特征,一直到没有这类特征的静态类型函数式语言(例如ML,Haskell,OCaml和F#等)。这其中部分是由于动态语言诸如Python,Perl和Ruby等的成功,在这些语言里,变量很少被显式声明。软件开发社区对于类型推断有着丰富的经验,并且也已经证明了使用这种技术以及创造和维护大型的代码并不会和现有的有什么冲突。

一些开发者被这个所困扰,就是使用auto使得快速看一眼源代码就知道一个对象的类型变得很难。然而,IDE的展示一个对象的类型的能力减轻了这个困扰(即使将Item 4中的提到的类型显示问题考虑在内),并且,在很多情况下,一个对象类型的某种抽象显示其实和完整的类型显示一样有用。我们经常只要知道某个对象是不是一个容器或者计数器或者智能指针就足够了,并不需要知道该对象是一个什么容器或者什么计数器或者什么智能指针。假设变量名都是精心设计的,这样的抽象类型信息应该可以随时知道。

事实就是显示类型声明经常会引起一些奇妙错误,要么影响准确性,要么影响性能,要么两者都影响。而且,auto类型会根据它们的初始化表达式变化而自动变化,这就意味着一些代码重构会受益于auto。例如,如果一个函数被声明成返回一个int,但是后来你决定返回一个long会更好,那么如果你将调用该函数的结果都写入一个auto变量,那么下次你再编译的时候,这些调用代码会自动更新它自己。但如果这些结果保存在显示声明的变量里,那么你就需要找到所有这些调用方并且修改它们。

要点记忆

  • auto变量必须被初始化,对于类型不匹配所造成的移植问题或者效率问题免疫,可以简化代码重构过程,并且一般相比于显示类型声明的变量,少打不少字
  • auto类型的变量满足Iem 2和Item 6中所描述到的陷阱
阅读全文
0 0