C++模板:过犹不及

类别:编程语言 点击:0 评论:0 推荐:

C++模板:过犹不及

[译者:孟岩先生引介的这篇文章,实在是振聋发聩。长久以来程序员间流传着一种不好的风气,就是技术(技巧?)至上。作为一种新生事物,泛型与STL也在理想与现实之间逡巡一个平衡点。遗憾的是,独木桥永远没有阳关道那么好走。
当然,程序员(尤其是初学者)也容易走入人云亦云的误区里去。笔者不希望看到社区再因为这篇文章掀起诸如“泛型和OO哪个更好”之类的无味争端。寂寞如独孤求败,草木皆可取人性命;鄙俗如你我,不如反躬自省“OO和泛型到底是什么,我真的懂了吗?”]

每隔十年左右,在编程界总会出现一些新生的时尚,这些时尚几乎无一例外的宣称自己是对过去某些缺陷的扬弃。而我们则会又一次以为从此软件将变得更加可靠、开发更加廉价、甚至开发过程都会变得更加有趣。(不过没有人相信软件会变得更小或者更快)在70年代,风靡一时的是结构化的程序设计方法;到了80年代,是面向对象的天下;而从90年代中期开始,则是泛型编程大行其道。这“泛型”二字颇有些来头,它是得自一种强大的代码复用技术——模板(包括泛型类和泛型函数)。

参数化的类和函数(即所谓之模板)确乎是斩将掣旗的利器。比如一个sqr()函数,它就可以被设计成可以对一切定义了乘法运算的类型进行平方运算——复数、矩阵、等等。象list<>之类的标准容器类都利用了模板——你不必为每种特定类型都重写一个实现。对于清汤寡水的老式C++来说,模板的确是一个实实在在的进步。我也觉得(C++的)ISO标准是一个巨大的进步。但是,在这个过程中,有些东西似乎有点过火。

例如,标准库中的string和iostream都是模板类,都要用“字符traits”类型进行参数化。这就意味着同样一个basic_string<>类的定义既可以生成ASCII字符串,也可以生成Unicode字符串,甚至火星人用的三字节字符串!(虽然原理上如此,但许多的实现还是只支持ASCII字符,实在是暴殄天物)标准委员会要求,这些几乎每个C++程序都要用到的常用类都必须以模板实现。

但是,为了获取更好的效能和对调试的支持是需要付出代价的。我做了个试验(使用微软Visual C++ 6.0)就发现了存在的问题。这个编译器既支持新式的iostream(模板)库,也支持“经典”的iostream类库,因此我们可以通过它来进行这两种不同实现的比较。第一个测试当然是“Hello, World”,这一回合里经典iostream以快两倍以上的编译速度胜出。另一个更复杂的程序有200行,每行都要输出10个变量。结果编译速度让人大跌眼镜:使用标准模板库版本的程序花了几乎整整10秒才编译完,与之对应的经典类库版本只用了1.5秒。10秒钟不是一个小数字了;一笔大生意很可能就在这看似短暂的一瞬中和你失之交臂。可执行代码大小呢,标准模板库版本的有115K,而经典类库版本的只有70K。在不同的机器上,具体数字可能不同,但我们已经可以看出总的趋势是使用新的iostream模板库的程序编译起来更慢而生成的可执行代码体积更大。而况这个问题也不单是微软的编译器所独有,GCC的表现也如出一辙。

当然,可执行代码体积大小的问题已经不象以前那么重要了,但最近各种作为信息载体的可编程设备(这些设备在今后的若干年里仍然要面临内存紧张的问题)发展势头相当迅猛:手持设备、移动电话、智能冰箱、支持蓝牙的咖啡壶等等。之所以使用标准iostream模板类的程序可执行代码体积会增加,是因为模板代码被全部内联到程序里去了。使用了模板技术之后,你就很难在优化关键操作的同时避免代码体积膨胀。另一方面,对我来说编译链接的时间长短更为重要,因为更长的编译时间意味着我要等得更久,而且会失去对于开发而言非常重要的“交互流动(Conversational Flow)”的可能。

然后,我们还要考虑调试的方便性问题。例如标准库里用模板实现的string类,设计可谓匠心独运,但程序的调试者看到的却是面目狰狞。她必须面对编译器和除错器给出的象下面这样完全展开之后的全名:

class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char>>

试想如果发生在非常有用的map<string, string>身上,这个名字将会如何不堪入目!名字太长,导致使用者总是会得到一大堆关于内部名称的编译警告。其实std::string这个名字对初学者来说特别容易顾名思义,所以他是不应该因为把这个名字当做语言内建的标记使用而受到惩罚的。如果让编译器在输出编译错误信息之前先搜索一遍在这个名字作用域里所有定义过的typedef,在技术上是完全可行的,而且我也会在UnderC项目里这样做。Verity Stob建议给C++的出错信息做一个后处理程序,我希望她是在开玩笑。更简单的方法是尽量不要使用过于复杂的类型。我在用C++开发时有一样秘密武器(我可是头一次公开这个秘密),就是在大项目里,用一个和<string>接口兼容的string类来代替前者。偶尔我也会用标准头文件重新编译项目,来检查我自己的库是不是仍然可信,但我一般都是让别人去为只能通过牺牲性能换取正确结果的问题伤脑筋。

我觉得,对于那些需要同时处理ASCII和Unicode字符串,或者需要自己定制内存分配策略等等的需求来说,确实存在非得借重std::string的弹性不可的程序。但这类程序不是那么常见(通常,同一个程序不是用ASCII就是用Unicode),而且为了追求所谓通用性而加重程序员的负担,似乎不那么公平。它没有使类库开发者的工作变得更有趣,但却让应用程序的编写者的工作变得更烦琐。设计良好的类库本应把实现上的难点隐藏起来,让人可以直接了当的使用,如今这样却是本末倒置。要知道,std::string的设计没有把它的实现充分隐藏起来,因为使用它的程序员在开发过程中会不断的意识到它的存在。而且我们不能保证使用这些类的人都是尖端科学家。制定标准的本意仅仅是为了规范各个类的公有接口和期望功能,但现行的C++标准一味的坚持某种特殊的实现策略,这与标准的本意根本就背道而驰。当然,通用的模板功能还是应该一直存在下去,但只是为那些确实需要的人而存在。

类似的问题也出现在象list<>之类的标准容器类上。这些类都带一个额外的模板参数用来指定一个内存分配器。虽然大多数人并不需要这个便利,但如果你确实用得着的话,它也的确很有用。我仍然认为,把这些通用性更强的版本单独定义成另外一个模板类比较好。我也知道,这样做的话标准类库在技术上就没有什么新鲜之处了,但类库首先应该适应最终用户的需要。用户不应该被那些他们用不到的东西骚扰。

除了把本不需要模板的东西设计成模板的危险之外,用C++进行泛型编程还有另外一个问题。大多数人都同意标准库的算法是很有效的。如果我有一个保存整数的vector,那么调用sort(v.begin(), v.end())就能给它排序。因为比较操作是内联的,这个泛型算法要比老式的qsort()来得快,而且也更容易使用,特别是在当这个vector保存的是用户定义类型的时候。而copy()则可以把任何东西复制到任何地方去,而且采用的是尽可能高效的算法。

但有些算法则是毫无必要的隐讳:

copy_if(v.begin(),v.end(),ostream_iterator<int>(cout) bind2nd(greater<int>(),7));

如果纯粹照本宣科的话,应该把每个名字都加上std::,不过我们假定所有的东西都已经被拿到全局名字空间里了,不管是通过using声明还是其它什么见不得人的方法。这个Stroustrup所举的例子,其实可以更老套的表达成把所有的整数前后相继的送入输出流里。这样反而更明确些:

vector<int>::iterator li;
 for (li = v.begin(); li != v.end(); ++li)
  if (*li > 7) cout << *li;

Stroustrup告诉我们,显式的循环是“冗长乏味和容易出错的”,但我看不出前一个版本有任何的优点。很明显,人们会慢慢熟悉这些符号;人的适应性是很强的,而且作为专业人士,我们也必须不断学习新的表达方式。但是这种新的表达方式显然更加“冗长乏味”,而且更不易读、更缺乏弹性。更有甚者,这种表达方式会束缚设计决策。比如,假设我们有一个列表保存一些Shape *指针,我们可以命令它们画出自己,或者这样:

for_each(ls.begin(),ls.end(),
     bind2nd(mem_fun(&Shape::draw),canvas));

或者是:

 ShapeList::iterator li;
 for (li = ls.begin(); li != ls.end(); ++li)
    (*li)->draw(canvas);

喏,如果我想做点改动,(特别是在不希望把动作加到图形类里的时候)我只需依照一定的规则画出图形,然后在显式的循环体里加入一个if语句就行了。如果我想使用泛型风格的写法,我只能实实在在的在for_each()算法的“payload”里定义一个函数了。如果使用《软件模式书2》上的术语的话,前一个例子是一个内部迭代子,而后一个例子是一个外部迭代子。书的作者认为C++并不很擅长表现内部迭代子,而我认为我们也应该尊重这种语言的局限性。问题出在对使用C++进行泛型编程的过分狂热上——这种狂热再次把我们引向不必要的困难。C++并不支持LISP、SmallTalk、Ruby等支持的匿名函数。匿名函数(或Λ表达式)在C++中应该类似下面的第三个例子;也许有一天谁就会实现它呢:

for_each(ls.begin(),ls.end(),
  void lambda(Shape *p) { p->draw(canvas); });

C++是一种非凡的语言,从移动电话到跨越重洋的网络,在一切地方你都能发现它的存在。它还可以天衣无缝的支持许多种不同的编程风格,尽管这种兼收并蓄也可能会造成问题。编程真正的艺术性在于针对特定问题选择合适的表达方式,就如写文章也要考虑读者群一样。现有的标准是许多人汗水的结晶,而且给我们提供了一个共同的平台,我无意破坏它。我的担忧是,现在的标准过于热衷于泛型编程风格,而变得更象是限定“什么是良好的编程风格”的敕令(比如,标准里的算法过于排斥显式循环)。对程序员来说,暴露过多的实现细节也成为一种负担(比如basic_string<>),这使得C++给人以“编程高手的语言”的印象。

 


 

本文地址:http://com.8s8s.com/it/it29686.htm