第34章 索引表
难度5
索引表确实是一种有用的惯用法(idiom),而且是一种值得去了解学习的技术。但我们如何才能有效地实现这一技术呢……等等,应当不仅是“有效”,“完美”怎么样?
JG问题
1. 谁会受益于清晰易懂的代码?
Guru问题
2. 以下代码展现了在已有容器中创建索引表的一种有趣且有用的惯用法。如需更详细的解释,请参考其原文[Hicks00]。
评价这段代码并找出:
a) 像无效语法或不可移植的风格习惯之类的“机械”错误。
b) 在风格上可以作哪些改进,使代码的清晰度、重用性和可维护性都得到改善。
// sort_idxtbl(…)的作用是排列一个索引数组
#include <vector>
#include <algorith>
template <class RAIter>
struct sort_idxtbl_pair
{
RAIter it;
int i;
bool operator<( const sort_idxtbl_pair& s )
{ return (*it) < (*(s.it)); }
void set( const RAIter& _it, int _i ) { it=_it; i=_i; }
sort_idxtbl_pair() {}
};
template <class RAIter>
void sort_idxtbl( RAIter first, RAIter last, int* pidxtbl )
{
int iDst = last-first;
typedef std::vector< sort_idxtbl_pair<RAIter> > V;
V v( iDst );
int i=0;
RAIter it = first;
V::iterator vit = v.begin();
for( i=0; it<last; it++, vit++, i++ )
(*vit).set(it,i);
std::sort(v.begin(), v.end());
int *pi = pidxtbl;
vit = v.begin();
for( ; vit<v.end(); pi++, vit++ )
*pi = (*vit).i;
}
main()
{
int ai[10] = { 15,12,13,14,18,11,10,17,16,19 };
cout << "#################" << endl;
std::vector<int> vecai(ai, ai+10);
int aidxtbl[10];
sort_idxtbl(vecai.begin(), vecai.end(), aidxtbl);
for (int i=0; i<10; i++)
cout << "i=" << i
<< ", aidxtbl[i]=" << aidxtbl[i]
<< ", ai[aidxtbl[i]]=" << ai[aidxtbl[i]]
<< endl;
cout << "#################" << endl;
}
解决方案
清晰度
1. 谁会受益于清晰易懂的代码?
简而言之,对所有人都有好处。
首先,清晰的代码更易于调试,也正因为清晰,所以代码在第一时间的错误也就少很多,就算目光再短浅,编写清晰的代码也至少可以让你的生活更轻松一些。(相关案例,请参考第27条款围绕示例27-3的讨论。)此外,当你一个月或一年之后重读你的代码时(如果你的代码当初没问题并投入了实际使用的话,这一环节通常是免不了的),你就会发现更容易“重拾”那些清晰的代码,明白代码都干了些什么。绝大多数程序员觉得要在头脑中记住代码的全部细节并保持哪怕只是几周时间都是很困难的,尤其是在转向其它的工作之后,要记住这些就更加困难了。经过几个月乃至几年之后,即便是重读自己以前写的代码,也很容易觉得那似乎是一个陌生人写的(只不过那个“陌生人”恰好跟你有同样的个人编码风格)。
利己方面已经说得够多了。让我们来看看有利于别人的方面:代码维护者也将获益于代码的清晰性和可读性。毕竟,要将代码维护好首先得“投入(grok)”代码。罗伯特.海因莱因(Robert Heinlein)杜撰了“grok”一词,意指深入而完整地理解;在此处,这个词还包含有理解代码本身内在的工作方式、代码的副作用以及其与其它子系统的交互方式的意思。总而言之,在没有完全理解一段代码的情况下就贸然去修改它太容易引入新的错误了。清晰、可理解的代码更容易让人投入其中,因此,对于这种代码的修补就变得不那么脆弱、危险,而且也不太容易无意间引入不想引入的副作用。
然而,最重要的一点是,由于以上这些原因,你的最终用户将得益于清晰、可理解的代码:这种代码从一开始错误就很少;更容易被正确地维护,而且在维护过程中也不至于引入同样多的错误。
指导方针:
一般来说,优先考虑编写清晰、正确的代码。
深入剖析索引表
2. 以下代码展现了在已有容器中创建索引表的一种有趣且有用的惯用法。如需更详细的解释,请参考其原文[Hicks00]。
评价这段代码并找出:
a) 像无效语法或不可移植的风格习惯之类的“机械”错误。
b) 在风格上可以作哪些改进,使代码的清晰度、重用性和可维护性都得到改善。
请允许我重复一遍:这些代码展示了一种有趣且有用的惯用法。我常常发现必须以不同的方式访问相同的容器,例如按照不同的排序准则来看待同一个容器中的元素。所以说这样的方法的确是很有用的:以一个主容器(譬如vector<Employee>)保存实际数据,以若干副容器保存指向主容器中元素的迭代器,这样一来我们就能支持各式各样的访问方式(例如,set<vector<Employee>::iterator, Funct>,其中Funct是用以间接比较Employee对象的仿函数,于是就可以产生不同于对象在vector中物理存储顺序的排序)。
话虽如此,风格也是很重要的。原作者爽快地允许我将他的代码作为相关案例,而我也并非在此对他的代码吹毛求疵;而只不过是通过剖析和批评已发布代码的方式来阐释编码风格原则罢了,很早以前诸如P.J.Plauger等人就已经这么做了。我从前评论过其它人发布的东西,同时也让别人来批评我的东西,而且我相信这一做法会得到更多人的认同和效仿。
说完了所有这些,让我们来看看究竟能对给出的代码案例作哪些改进。
更正“机械”错误
a) 像无效语法或不可移植的风格习惯之类的“机械”错误。
建设性的批评的首要方面就是代码中的“机械”错误。像下面列出的这些“机械”错误在大多数平台上是无法通过编译的。
#include <algorith>
1. 正确拼写标准库头文件名。在这个例子中,头文件<algorithm>被误作<algorith>。我首先猜测,这也许是因为原先代码的测试环境是一个8字符文件名的系统的缘故,但即便是我的老版本Windows(基于8.3文件名系统)上的老版本VC++也无法编译这样的代码。就算是在那些限制颇多的文件系统上,编译器本身也是被要求能支持任何标准长度的头文件名的(即便编译器只不过是在背后悄悄将它映射成较短的文件名(或根本不映射到文件上))。
接下来,考虑:
main()
2. 正确地定义main函数。这种无修饰的main函数原型从来都不是标准C++[C++98]风格的,虽说它也可以作为一个合法的编译器扩展特性(前提是编译器得给出警告)。这种main函数原型在C99之前是有效的,因为当时的C里面允许所谓的“隐式int声明”,但这在C++(C++里从来就不允许隐式int)和C99[C99](C99甚至果断地将这一特性彻底从标准中剔除掉了)中都是非标准的。在C++标准中,请参看:
§3.6.1/2:可移植代码必须将main定义为int main()或int main(int,char*[])两种形式中的一种。
§7/7 脚注78,和§7.1.5/2脚注80:隐式int是被禁止的。
附录 C(兼容性),对7.1.5/4的注释:明确指出main()这种形式在C++中是无效的,必须写作int main()。
指导方针:
不要依赖隐式int;这不是符合标准的可移植的C++。特别地,“void main()”或光是“main()”从来就不是标准C++写法(虽然仍有很多编译器将它们作为扩展加以支持)。
cout << “#################” << endl;
3. 永远记得#include你所需要的类型定义的头文件。这个程序使用了cout和endl但却没有#include<iostream>。那为什么这在代码原作者的系统上能工作呢?这是因为C++标准头文件会互相#include,但不像C,C++并没有指定哪些标准头文件#include哪些其它标准头文件。在这个案例中,程序有#include <vector>和<algorithm>,而在原来的那个系统上,也许恰好某个头文件间接地#include <iostream>了。这在原代码所使用的特定的库实现上或许是行得通的,甚至在我这恰巧也能正常工作,但它并不是可移植的,而且也不是种好风格。
4. 遵循《More Exceptional C++》[Sutter02]的条款39中关于使用名字空间(namespace)的原则。就cout和endl而言,程序必须以std::限定它们,或者像这样写:using std::cout; using std::endl;。不幸的是,忘记名字空间域限定符的情况仍然很普遍。我得赶紧指出,这段代码的原作者对vector和