条款47:避免产生只写代码
条款47:避免产生只写代码
假设你有一个vector<int>,你想去掉vector中值小于x而出现在至少和y一样大的最后一个元素之后的所有元素。下面代码立刻出现在你脑中吗?
vector<int> v; int x, y; ... v.erase( remove_if(find_if(v.rbegin(), v.rend(), bind2nd(greater_equal<int>(), y)).base(), v.end(), bind2nd(less<int>(), x)), v.end());
一条语句就完成了工作。清楚并直接了当。没问题。对吗?
好,让我们先退回一步。对你来说它是合理的、好维护的代码?“不!”大部分C++程序员惊叫,他们的声音中充满了害怕和讨厌。“是!”他们中的一部分显然高兴地说。但那儿有一个问题。一个程序员很有表现力的方法是另一个程序员的噩梦。
正如我所见,有上面代码涉及到两个问题。首先,它是函数调用的鼠巢。要知道我是什么意思,这里有一个相同的语句,但所有的函数名都替换为fn,每个n对应一个函数:
v.f1(f2(f3(v.f4(), v.f5(), f6(f7(), y)),.f8(), v.f9(), f6(f10(), x)), v.f9());
这看起来不自然地复杂,因为我去掉了出现在原例中的缩进,但我想它可以很明白的表示了如果一条语句使用了12次函数调用,分别调用了10个不同的函数,那会被大部分C++程序开发人员认为很过分。但曾经用过比如Scheme的函数式语言(functional language)的程序员可能感觉不同,而我的经验是大多数看到原来的代码而没有感到惊讶的程序员有一个很强的函数式语言背景。大部分C++程序员缺乏这样的背景,所以除非你的同事精通深度嵌套(nest)的函数调用的方法,类似上面erase调用的代码几乎可以肯定会打败下一个想要弄明白你写的代码的人。
这段代码的第二个缺点是需要很多STL背景才能明白它。它使用了不常见的_if形式的find和remove,它使用了逆向迭代器(参见条款26),它把reverse_iterator转换为iterator(参见条款28),它使用了bind2nd,它建立了匿名函数对象而且它使用了erase-remove惯用法(参见条款32)。有经验的STL程序员可以轻易地吞下那些组合,但更多C++开发者在一口吃掉之前会一遍一遍看。如果你的同事对在一条语句中使用erase、remove_if、find_if、base和bind2ndSTL的这种方法都很熟悉,那可能很好,但如果你想让你的代码易于被主流背景更多的C++程序员了解,我鼓励你把它分解为易于消化的块。
这是一种你可以使用的方法。(注释不光为了本书。我也会把它们放入代码。)
typedef vector<int>::iterator VecIntIter; // 把rangeBegin初始化为指向最后一个
// 出现一个大于等于y的值后面的元素。
// 如果没有那样的值,
// 把rangeBegin初始化为v.begin()。如果那个值的
// 最后一次出现是v中的最后一个元素,
// 就把rangeBegin初始化为v.end()。 VecIntIter rangeBegin = find_if(v.rbegin(), v.rend(), bind2nd(greater_equal<int>(), y)).base(); // 从rangeBegin到v.end(),删除所有小于x的值 v.erase(remove_if(rangeBegin, v.end(), bind2nd(less<int>(), x)), v.end());
这仍然可能把一些人弄糊涂,因为它依赖于对erase-remove惯用法的了解,但在代码的注释和一本好的STL参考(比如,Josuttis的《The C++ Standard Library》[3]或SGI的STL网站[21])之间,每个C++程序员应该可以不太困难地指出它作了什么。
当转换代码时,要注意我并没有放弃算法并试图自己写的循环。条款43解释了为什么那一般是一个劣等的选择,它的论点在这里。当写源代码时,目标是写出对编译器和人都有意义的代码,并提供可接受的性能。算法基本上总是最好地达到那个目标的方式。但是,条款43也解释了增加算法的时候会自然导致增加嵌套函数调用并大量使用绑定器和其他仿函数适配器。再看看本条款开头的问题描述:
假设你有一个vector<int>,你想去掉vector中值小于x而出现在至少和y一样大的最后一个元素之后的所有元素。
一个方案的轮廓跳入脑中:
- 找到vector中一个值的最后一次出现需要用逆向迭代器调用find或find_if的某种形式。
- 去掉元素需要调用erase或erase-remove惯用法。
把这两个想法加在一起,你可以得到这个伪代码,其中“something”表示一个用于还没有填充的代码的占位符:
v.erase(remove_if(find_if(v.rbegin(), v.rend(), something).base(), v.end(), something)), v.end());
一旦你完成了这个,确定something就不是很难了,下一个事情你是知道的,你有原例中的代码。那是为什么这种语句一般被称为“只写(write-only)”代码。当你写代码时,它似乎很直截了当,因为它是一些基本想法(也就是,erase-remove惯用法加上使用逆向迭代器调用find的想法)的自然产物。但是,读者们很难把最后的产物分解回它基于的想法。这就被称为只写代码:很容易写,但很难读和理解。
代码是否是只写依赖于读它的人。正如我提到的,有些C++程序员不反感本条款中的代码。如果这是你工作环境的典型而且你希望它在未来也典型,那就自由释放出你最多的高级STL编程爱好。但是,如果你的同时不适应函数式编程风格而且STL经验很少,收回你的野心,写一些接近我前面演示的两条语句的方法的东西。
代码的读比写更经常,这是软件工程的真理。也就是说软件的维护比开发花费多得多的时间。不能读和理解的软件不能被维护,不能维护的软件几乎没有不值得拥有。你用STL越多,你会感到它越来越舒适,而且你会越来越多的使用嵌套函数调用和即时(on the fly)建立函数对象。这没有什么错的,但永远记住你今天写的代码会被某个人——也可能是你——在未来的某一天读到。为那天做准备吧。
当然,使用STL,好好使用,有效使用。但避免产生只写代码。最后,这样的代码完全不高效。