《Exceptional C++ Style》开放样章译稿

感谢老朋友谢轩(《Symbian OS Explained》译者)无私提供原译稿,这是调整过的版本。因为是原公开样章,所以放在blog上。

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你所需要的类型定义的头文件。这个程序使用了coutendl但却没有#include<iostream>。那为什么这在代码原作者的系统上能工作呢?这是因为C++标准头文件会互相#include,但不像CC++并没有指定哪些标准头文件#include哪些其它标准头文件。在这个案例中,程序有#include <vector><algorithm>,而在原来的那个系统上,也许恰好某个头文件间接地#include <iostream>了。这在原代码所使用的特定的库实现上或许是行得通的,甚至在我这恰巧也能正常工作,但它并不是可移植的,而且也不是种好风格。

4. 遵循《More Exceptional C++[Sutter02]的条款39中关于使用名字空间(namespace)的原则。coutendl而言,程序必须以std::限定它们,或者像这样写:using std::cout; using std::endl;。不幸的是,忘记名字空间域限定符的情况仍然很普遍。我得赶紧指出,这段代码的原作者对vectorsort做了正确的名字限定,这倒很不错。

改进风格

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.复用(第四部分),或者叫“尽量使用!=来比较迭代器”:进行迭代器的比较时,务必使用!=”!=”对各种类型的迭代器都可用)而不是 < ”<”只对随机访问迭代器有效),当然除非你真的一定要使用 < 而且你刻意只支持随机访问迭代器。原程序中使用了 < 来比较迭代器,这对于随机访问迭代器没有问题,而程序的本意也就是创建索引表放入vector和数组中,而这两者都支持随机访问。但是,我们没理由不想让这些功能可以作用在诸如listset的其它不支持随机访问的容器上啊!而原代码之所以不能适用于这些容器正是因为使用了 < 而不是!=来比较迭代器所致。

正如Scott Meyers[Meyers96]条款32中阐述的,“在‘将来时态’下编程。”他论述道:

好的软件能够适应变化。它能够容纳新的特性,移植到新的平台,满足新的需求,处理新的输入。具有这等灵活性、健壮性、可靠性的软件并非天上掉馅饼。它是程序员们在满足了现在的需求并关注了将来的可能性之后设计和实现出来的。这样的软件(能够温和地适应变化)是那些在未来时态下开发程序的人写出来的。

指导方针:

尽量使用 != 而非 < 来比较迭代器。

10.除非你真的需要旧的值,否则尽量使用前置递增。这里,对于迭代器而言,应当习惯性地使用前置递增(++i),避免后置递增(i++);详见[Sutter00]条款6。诚然,对于原先的代码来说这两种做法或许不会带来本质上的差别,因为vector<T>::iterator很可能是复制开销很小的T*(尽管也许不一定是这样——这是由STL实现决定的),但如果我们要落实第9条建议,就不能将眼光局限在vector<T>::iterator上面,况且大多数其它的迭代器都是类类型的,或许其复制的开销仍然不算很昂贵,可我们何必要无谓地引入这一潜在的效率损失呢?

指导方针:

尽量选择使用前置递增而非后置递增,除非你确实需要使用旧的值。

上面几点已经覆盖了大多数重要的问题。不过还是有一些可以拿来评论的东西,只是我觉得没有必要把注意力放在这些不是很重要的问题上;例如:产品代码中应当有说明类和函数的用途及语义的注释,但这不适用于杂志文章所附的代码,因为文章里会有语言组织得更好、更为详细的解释。

我故意没有对主函数的风格进行评价(这跟上面提到的“机械”错误有所区别,后者如果不加以更正的话将会导致代码无法通过编译),因为毕竟这个主函数只是个演示的工具,它帮助读者了解索引表这个设施是如何使用的,而索引表本身才是焦点所在。

小结

让我们在保持原代码的接口的基础上换一种设计。40 把我们的改动限定在改正代码中的“机械”错误和基本风格的范围内,然后,考虑下面三种改进版本。它们每一个都有其各自的优点、缺点以及风格偏好,在代码注释中有相应的解释。这三个版本的共同点就是更为清晰、更易理解、更适于移植:这三点对你我的公司来说应该是很有价值的。

// [Hicks00]中的代码的一个改进版本

//

#include <vector>

#include <map>

#include <algorithm>

// 解决方案1:进行了一些基本的清理工作,但仍保留了原先代码的大致结构。

// 代码量缩减至17行(即使你把public:private:也算上的话),而原先的代码有23行。

//

namespace Solution1 {

template<class Iter>

class sort_idxtbl_pair {

public:

void set( const Iter& it, int i ) { it_ = it; i_ = i; }

bool operator<( const sort_idxtbl_pair& other ) const

{ return *it_ < *other.it_; }

operator int() const { return i_; }

private:

Iter it_;

int i_;

};

// 绝大多数改动都在该函数中,你可以看到,该函数现在只有5行,而原先有13行。

// 在每行代码之后,我给出了原来的代码以供比较。

// 尽量编写清晰而简洁的代码,不要引入不必要的复杂性和模糊性!

//

template<class IterIn, class IterOut>

void sort_idxtbl( IterIn first, IterIn last, IterOut out ) {

std::vector<sort_idxtbl_pair<IterIn> > v(last-first);

// int iDst = last-first;

// typedef std::vector< sort_idxtbl_pair<RAIter> > V;

// V v(iDst);

for( int i=0; i < last-first; ++i )

v[i].set( first+i, i );

// 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() );

// std::sort(v.begin(), v.end());

std::copy( v.begin(), v.end(), out );

// int *pi = pidxtbl;

// vit = v.begin();

// for (; vit<v.end(); pi++, vit++)

// *pi = (*vit).i;

}

}

// 解决方案2:使用std::pair而不是重新发明一个类似pair的辅助类。

// 现在代码从原先的23行降到了12行。其中有8行的意图是特定于本问题的,

// 剩下4行是可复用于其它情况下的。

//

namespace Solution2 {

template<class T, class U>

struct ComparePair1stDeref {

bool operator()( const std::pair<T,U>& a, const std::pair<T,U>& b ) const

{ return *a.first < *b.first; }

};

template<class IterIn, class IterOut>

void sort_idxtbl( IterIn first, IterIn last, IterOut out ) {

std::vector< std::pair<IterIn,int> > s( last-first );

for( int i=0; i < s.size(); ++i )

s[i] = std::make_pair( first+i, i );

std::sort( s.begin(), s.end(), ComparePair1stDeref<IterIn,int>() );

for( int i=0; i < s.size(); ++i, ++out )

*out = s[i].second;

}

}

// 解决方案3: 只是为了展示一些细节方面的替代做法,下面的代码中使用了一个multimap来消除

// 单独的排序步骤(译注:因为map是有序的),并使用std::transform()来代替手写循环。

// 仍然有13行代码,不过更多代码成为了可复用的。该方案使用了更多空间开销,而且很可能

// 时间开销也更大,因此我倾向于使用解决方案2。只不过该方案展示了寻找问题的替代方案的过程。

//

namespace Solution3 {

template<class T>

struct CompareDeref {

bool operator()( const T& a, const T& b ) const

{ return *a < *b; }

};

template<class T, class U>

struct Pair2nd {

const U& operator()( const std::pair<T,U>& a ) const { return a.second; }

};

template<class IterIn, class IterOut>

void sort_idxtbl( IterIn first, IterIn last, IterOutIterOut out ) {

std::multimap<IterIn, int, CompareDeref<IterIn> > v;

for( int i=0; first != last; ++i, ++first )

v.insert( std::make_pair( first, i ) );

std::transform( v.begin(), v.end(), out, Pair2nd<IterIn const,int>() );

}

}

// 测试用例基本没有变动,只不过输出结果被导入到输出迭代器(原来是一个int*

// 所指的区间(这里是一个vector)中去,同时直接利用ai作为容器。

//

#include <iostream>

int main() {

int ai[10] = { 15,12,13,14,18,11,10,17,16,19 };

std::cout << "#################" << std::endl;

std::vector<int> aidxtbl( 10 );

// use another namespace name to test a different solution

Solution3::sort_idxtbl( ai, ai+10, aidxtbl.begin() );

for( int i=0; i<10; ++i )

std::cout << "i=" << i

<< ", aidxtbl[i]=" << aidxtbl[i]

<< ", ai[aidxtbl[i]]=" << ai[aidxtbl[i]]

<< std::endl;

std::cout << "#################" << std::endl;

}



40 原作者([Hicks00]的作者)也报告了来自另外一个读者的反馈,他展示了另一种优雅的,但完全不同的方法:他创建了一个类似容器的对象,该对象包含了原始容器及其迭代器,而且允许使用不同顺序的迭代访问(译注:实际上,Boost库(www.boost.org)中已经有了一个非常重量级的实现,称为multi_index_container)。

你可能感兴趣的:(exception)