高效的代码重用是良好的软件工程中重要的一部分。为了演示如何更好地通过使用标准库算法而不是手工编写,我们再次考虑先前的问题。演示通过简单利用标准库中已有的算法来避免的一些问题。
Problem
JG Question
1. 最广泛使用的C++库是什么?
Guru Question
2. 首先,在GotW #2中有多少陷进是可以避免的,如果程序员只是用以下方法替代显示的基于迭代器的for循环:
(a)一个基于范围的for循环?
(b)一个标准库算法调用?
(注意:和GotW #2一样,不要改变函数的语义,即使它们有些地方是可以改进的)
为了简单说明,下面是大部分都已经修正了的函数:
string find_addr( const list<employee>& emps, const string& name ) { for( auto i = begin(emps); i != end(emps); ++i ) { if( i->name() == name ) { return i->addr; } } return ""; }
Solution
1. 最广泛使用的C++库是什么?
在每个平台上的实现的C++标准库。
2. (a)使用基于范围的for循环可以避免多少GotW #2中的陷进?
在GotW #2中,机灵的读者可能会说:“为什么你不使用基于范围的for循环?”的确,为什么不?它还会解决一些临时对象问题,不要介意它写的那么简单。
比较先前没有改进的显示迭代器循环:
for( auto i = begin(emps); i != end(emps); i++ ) { if( *i == name ) { return i->addr; } }
和使用基于范围的for循环(如果你记得写上const auto&还会加分)
for( const auto& e : emps ) { if( e == name ) { return e.addr; } }
表达式e==name和return e.addr在可能会在实际的临时对象方面都没有变化。但是裸循环代码中的不论是=引发的临时对象(回想一下:它没有),还是end()重新计算且应该保存在循环外(回想一下:可能不会,但也许),还是i++应该被重写成++i(回想一下:它应该这么做),这些问题在基于范围的循环中都得到了解决。这就是清晰代码的能力,同时使用了更高的抽象。
使用基于范围的for循环的关键优点是增了代码的抽象层次和信息密度。考虑一下:下面两行代码中,在不继续阅读接下来的代码时,你能想到什么:
for( auto i = begin(emps); i != end(emps); i++ ) { // A for( const auto& e : emps ) { // B
乍看起来,A行和B行都传达了相同的信息,但却不是这样。当看到A行时,你所知道的所有就是那是个循环,使用迭代器迭代了emps。我们如此习惯于于A,以致于我们眼睛次要的视觉都趋于在我们脑子里将它“自动补全”为“一个顺寻访问emps元素的循环”,且我们的自动补全通常是对的,除了当它不是:那是个++还是s+=2的循环步幅?在循环体内索引被修改了?我们次要的视觉可能会出错。
另一方面,B行传达了更多的信息给读者。当你看到B行时,在没有检查循环体的情况下你能确信的知道,它是一个顺序访问emps元素的循环,除此之外,你还简化了循环控制,因为没有简洁实用迭代器的必要。所有的这些都提升了代码的抽象层次,这是个好事。
要注意一点,在GotW #2的讨论中,裸for循环在没有通过追加附加的变量执行额外的计算(一个默认的构造函数紧接着一个赋值操作,而不只是一个构造函数)而导致代码更为复杂的情况下,是没有很自然地允许合并到单个return语句的。这对于基于范围的for循环来说还是一样的,因为它依旧是两条return语句在不同的范围内。
2. 。。。使用标准库算法调用?
在没有其他的改变前提下,简单实用标准的find算法做了基于范围for的所有事情,且又避免了不必要的临时对象。
// Better (focusing on internals) // string find_addr( /*...*/ ) { const auto i = find( begin(emps), end(emps), name ); // TFTFY return i != end(emps) ? i->addr : ""; }
这和基于范围的for版本一样,都消除了相同的临时对象,并且它进一步加强了抽象的层次。我们一眼就能看出,这是一个顺序访问emps元素的循环,但在上一层我们知道是在尝试find一些东西然后返回第一个匹配(如果存在)元素的迭代器。我们依旧间接使用了迭代器,但只是一个一次性的迭代器对象,而不是像原始for版本中的那样对迭代器进行算术运算。
更进一步来说,我们消除了整个循环嵌套域,并且将函数拉平到了一行进行这样简单的函数调用,这是基于范围的for做不到的。为了演示这里一些更基本的要点,注意在拉平函数是还做了其他什么。现在,因为return语句都在一个作用域范围上(可能只是因为我们消除了循环体),我们可以选择地组合它们。当然也可以依旧写成
if( i != end(emps) ) return i->addr; else return “”;
这样,可以一行或者两行或者四行,但是我们没有必要这么做。为了清晰起见,这里的重点不是减少return语句为目的,它也不该是,并且如我们在GotW #2所说,“单退出”总是有缺陷的。这里的重点是使用算法经常可以简化我们的代码不只是一个显示循环,甚至一个rang-for循环。不仅直接通过删除额外的间接对象和额外的变量以及循环嵌套,而且经常也允许在周边代码进行额外的简化。
上述代码在使用一个string比较employee时依旧可能会引发临时对象,如果我们进一步使用一个自定义的比较函数子的find_if的话,那么我们也将比较产生的临时对象都消除了。假设有一些比如employee::name()是可用的就像在GotW #2中一样,将这个于其他的修改组合起来,我们得到了:
// Better still (complete) // string find_addr( const list<employee>& emps, const string& name ) { const auto i = find_if( begin(emps), end(emps), [&](const auto& e) { return e.name() == name; } ); return i != end(emps) ? i->addr : ""; }
Summary
当你有一个或者可以编写一个合适的算法来完成我们想要的,倾向于使用算法而不是显示的循环。它可以提升我们代码的抽象层次和清晰度。Scott Meyers在Effective STL中的建议依旧是有效的。尤其是现在有着以前没有的lambda。
Guideline:相对于显示的循环,倾向于选择使用算法。算法调用经常可以使代码更清晰且减少复杂度。如果没有合适的算法,那就编写一个。因为我们可能会再次使用它。
优先使用已经存在的库代码而不是自己从无到有地手动编写。越是广泛地使用库,对于许多常见的需求,它就更可能是精心设计的,预调试和预优化的。还有什么比标准库更广泛使用的呢。在你的c++程序中,你的标准库的实现是你可能会使用最广泛使用的库代码。这有助于不管是库的设计还是实现:它所有的代码的目的是被使用和重用,并且很多思想都已经设计到了这些特性中,包括像find和sort这样的标准算法。库的实现者在库的效率、可用性和所有的注意事项方面都下了很大功夫,因此你没有必要---包括执行优化,应该从不依赖应用程序级别的代码,比如使用不可移植的OS或者特定CPU的优化。
因此,总是倾向于重用代码,特别是算法和标准库。要跳出“我编写那些代码只是因为我能”的陷阱。
Guideline:重用代码,特别是标准库代码,而不是自己手动编写自己的版本,因为它更快,更简单,更安全。