难度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和sort做了正确的名字限定,这倒很不错。
改进风格
b) 在风格上可以作哪些改进,使代码的清晰度、重用性和可维护性都得到改善。
除了“机械”错误之外,在代码案例中还有些地方如果要我来实现的话我会以不同的方式来完成。首先,要对那个辅助性结构sort_idxtbl_pair作两点评价:
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() {}
};
1. 务必保证const正确性(const-correctness)。在此例中,sort_idxtbl_pair::operator< 并不会修改*this,所以它应当被声明为const成员函数。
指导方针:
落实const正确性。
2. 消除冗余代码。该程序显式地写出了类sort_idxtbl_pair的默认构造函数,然而它却与隐式生成的版本没有丝毫区别。所以说可以干脆省去。此外,sort_idxtbl_pair的两个数据成员本就是公有的,那个set()成员函数虽说可以让设置这两个成员时的语法稍微好看一点,但由于它只在一处地方被调用过,因此就为这点好处并不值得引入这一额外的复杂性。
指导方针:
避免代码重复和冗余。
下面我们进入核心函数sort_idxtbl():
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;
}
3.选择有意义且合适的名字。这个案例中,sort_idxtbl是个具有误导性的名字,因为这个函数并不是在对一个索引表进行排序...而是创建了一个索引表!另一方面,代码使用了模板参数名字RAIter来指出这是个随机访问的(Random-Access)迭代器,这一点可以得个不错的分数;因为这个随机访问特性正是该版本的代码所必需的,所以将模板参数命名为RAIter可以作为一个很好的提示。
指导方针:
选择清晰而有意义的名字。
4.确保一致性。在sort_idxtbl()中,有时变量是在for循环初始化语句中被置初值的,而有时却又不是这样。这就使得代码更难以阅读,至少对我来说是这样。这一点所带来的好处因人而异。
5.消除不必要的复杂性。这个函数里面有好几处使用了不必要的局部变量!第一,变量iDst被初始化为last-first,但在后面却只被使用了一次;为什么不在使用处直接写last-first来摆脱这种混乱的状况呢?第二,vector的迭代器vit被创建的地方其实完全可以使用下标索引来代替,效果一样,而且代码也会更清晰些。第三,局部变量it被初始化为函数参数first的值,而在此之后first本身就再没被使用过;我个人倾向于直接使用函数参数(即使你改变参数的值也完全没有问题,因为是按值传递的),而不是引入另一个名字。
6.复用(第一部分):更多地复用标准库。 现在,原先的程序因为复用std::sort而拿了个高分,这挺不错。但是干嘛不采用std::copy而要手工实现最后的那个循环来完成复制工作呢?为什么要重新做一个只比std::pair多一个比较函数的专用的sort_idxtbl_pair呢?除了写起来更简单,复用还使你的代码更具可读性。谦虚一点,复用已有的代码吧!
指导方针:
了解并在任何适当的地方使用(复用)标准库设施,而不是自己去手动实现。
7.复用(第二部分):令实现本身更易于被复用(一石二鸟)。原先的代码中除了函数本身之外并没有任何东西是可以直接拿来复用的。外覆类sort_idxtbl_pair与它的用途捆绑得太紧,它根本不是独立可复用的。
8.复用(第三部分):改进函数的原型。原先的函数原型如下:
template<class RAIter>
void sort_idxtbl(RAIter first, RAIter last, int* pidxtbl)
它用一个光秃秃的int*指针指向输出区,而我通常会避免这样做,我更倾向于使用托管的存储空间(比如vector)。不过有一点是明确的:最终用户要能够调用sort_idxtbl并将输出放到一个普通数组或一个vector或是其它什么东西里。很明显,“能够将结果输出到任何容器”这样的要求正是迭代器的用武之地,不是吗?(参看条款5和条款6。)
template< class RAIn, class Out >
void sort_idxtbl( RAIn first, RAIn last, Out result )
指导方针:
避免不必要的类型硬编码,从而扩展泛型组件的可复用性。
9.复用(第四部分),或者叫“尽量使用!=来比较迭代器”:进行迭代器的比较时,务必使用font-size: 9pt; font-famil