Effective Modern C++ Item 5

来源:互联网 发布:团队优化的原则包括 编辑:程序博客网 时间:2024/06/08 15:40

Item5:宁以auto替代显式声明

啊,写出下面的代码是简单愉快的,

int x;

等一下,该死。我忘记初始化了,因此它的值是不确定的。它可能已被初始化为0。但令人叹息的是这个要依赖上下文。

没关系。让我们转向一个简单快乐的通过对迭代器解引用来初始化声明的局部变量的例子:

template <typename It>      //算法dwim(“do what I mean”)void dwim(It b, It e)       //用于所有在范围b到e的所有元素{    while(b != e) {        typename std::iterator_traits<It>::value_type          currValue = *b;        ...    }}

呃,“typename std::iterator_traits<It>::value_type”是用于表示迭代器指向值的类型?真的吗?我一定把这是多么有趣的事忘记了。该死。等等—我是不是已经说出来了?

好的,简单愉快的事(这是第三个):声明一个类型为闭包的局部变量是愉快的。噢,当然。只有编译器才知道闭包的类型,因此令人叹息的是不能写出其类型。该死。

该死,该死,该死!使用C++编程并没有它本应有的愉快体验!

嗯,它过去确实没有。但从C++11时起,由于auto,所有的这些问题都消失了。auto变量通过对初始化器的推导来得到它的类型,因此它必须被初始化。这意味着当你在现代C++11高速公路飞驰而过时,你可以对许多未初始化参数问题挥手说再见了:

int x1;             //可能未初始化auto x2;            //错误!需要初始化auto x3 = 0;        //好的,x的值是良好定义的

上述的高速公路没有与声明的值为解引用迭代器的局部变量有关的坑洼:

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::unique_ptr     const std::unique_ptr<Widget>& p2)         //指向的Widgets{ return *p1 < *p2; };

十分的酷。在C++14中,可以变得更酷,因为匿名表达式的参数可使用auto

auto derefLess =                                //C++14比较函数,  [](const auto& p1,                            //用于任何类指针指     const auto& p2)                            //向的值{ return *p1 < *p2; };

尽管很酷,可能你认为我们不是真正的需要使用auto来声明带有闭包的变量,因为我们能用std::function对象。我们确实可以,但这可能不是你所想的。而且现在你可能在想“std::function对象是什么?”所以让我来解释一下。

std::function是C++11标准库中的一个模板,它是函数指针的泛化。而函数指针只能指向函数,但是std::function适用于任何可调用对象,换句话说,就是能像函数一样被调用的任何对象。只是当你创建一个函数指针时,你必须制定函数指向的类型(换句话说,你想要指向的函数的签名),当你创建一个std::function对象时,你必须指向要引用的函数类型。你通过std::function的模板参数来做到这个。例如:声明一个名为funcstd::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>&)>  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对象调用闭包会比通过使用auto对象调用慢。换句话说,std::function方法通常比auto方法大且慢,而且它会产生内存溢出的异常。此外,正如你在上面所看到的例子,写出“auto”比写类型为std::function的一个实例简单的多。在auto与持有闭包的std::function的比赛中,这就是为auto所安排的。(一个相似的推论也适用于持有调用std::bind结果的autostd::function,但在条款34中,无论如何,我将尽全力的说服你使用匿名表达式而不是std::bind)。

auto的优点不只有避免未初始化变量和冗长的语法,以及可以直接的持有闭包。它还能避免我称为“type shortcuts。”这些代码你可能见过—甚至可能写过:

std::vector<int> v;...unsigned 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位Windows下工作的代码可能会在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::unordered_map的Key部分是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){    ...                         //和之前一样}

这不仅更高效而且还容易写。这个代码有一个非常诱人的特性,如果你对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的变量中,你就要找到所有的调用位置来修改它们。

要记住的事:

  • auto变量必须被初始化,而且通常可以免除类型匹配而导致移植或效率的问题,可以减少重构的过程,而且一般需要比显式指定类型变量更少的拼写。
  • auto类型变量将受限于条款2和6中所描述的缺陷。