Programming 版 (精华区)

发信人: zhangyan (让暴风雨来得更猛烈些吧), 信区: Programming
标  题: C++模板 过犹不及
发信站: 哈工大紫丁香 (2002年04月11日08:28:56 星期四), 站内信件

标题     C++模板:过犹不及    zhangyan_qd(翻译) //ft 又找到一个zhangyan
  
关键字     C++ Template 泛型 STL 
  
出处     http://www.imformit.com 
  


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++给人以“编程高手的语言”的印象。

 


 

 
 

对该文的评论  
      optimizer (2002-4-10 19:58:41)  

孟岩的后半段话应该是由感而发,虽有偏激之嫌,
但基本上应是任何一名做过实际大型应用的程序员的
共同感受。同解决某个领域的实际问题相比
(特别是当这个领域具备一定的挑战性的时候),
语言可能算是最简单、最容易掌握的东西。 
 
      anrxhzh (2002-4-10 19:56:58)  

在上周末的“世纪大讲堂”上范曾讲过这样的话:“艺术是需要难度的”。从这个角
度看,就不难理解C++标准库的所谓“过度设计”。 
 
      ylm163net (2002-4-10 19:33:16)  

说出了我的心里话 
 
      myan (2002-4-10 18:21:27)  

感谢张岩把这篇文章翻译出来,翻译得很精彩。

这篇文章的大部分观点我确实是赞成的。C++标准库是我所见到的最灵活也最复杂的
库。我对于STL的感觉一直很好,但是除了STL,C++标准库还有很多东西。其实真正
令人恐惧的是string, stream和locales,这三个部分之间错综复杂的关系,即使你
大致的看一眼,也会觉得冷汗直冒。在翻译《The C++ Standard Library》后半部分
的时候,我一边感叹标准库设计者的伟大,一边在脑子里的阴暗角落里不断地蹦出一
个词:“过度设计”。

C++是一种语言,但是归根到底也只是一种语言。在整个人类信息化的过程中,程序
设计语言毕竟只是很小的一部分。你不能仅靠编程技巧就解决声音控制和思维传感问
题,你也不能仅靠面向对象分析与设计就解决CIMS系统的种种复杂问题。或许你可以
凭借自己高超的技巧和出色的分析设计能力改善你自己的生活,可是千万不要因此而
过高估量自己以及自己所掌握的技术在整个社会发展中的重要性,更不要小看其他人
的工作和努力。一个编程技术远不如你的人,可以在他的专业技术领域用很烂的编程
技术实现一个你永远也做不出来的产品。


 
      anrxhzh (2002-4-10 18:20:59)  

多谢gigix的指教,虽然还没完全搞懂:-)
STL在GP领域占有很重要的地位,虽然理论上STL的载体不一定必须是C++,但是现实
为什么选择了C++呢?忘不啬赐教。

 
      j_q_song (2002-4-10 18:20:32)  


可能有更好的编译器的出现,vc6.0编译的慢或对stl的支持的不好
只是因为那时stl尚未成为气候,慢慢大家接受以后
你再看怎样? 
 
      gigix (2002-4-10 17:16:25)  

另外,如果要让我说C++有什么东西不好,我觉得不是模板,而是强类型。这是最近
几个月看python养成的毛病:可以要类型,但不喜欢类型的束缚。不过,如果没有了
强类型,C++还是C++吗? 
 
      gigix (2002-4-10 17:10:47)  

to anrxhzh:
“至少到目前为止,还没有那种编程语言比C++更出色地给GP提供了表演的舞台。”


不敢苟同。至少在我看来,用python来实现GP就比用C++容易得多。不过这样说没什
么意义,我们来回顾一些面向对象的基础知识。

多态(polymorphism)有两种划分方法:完全的(total)和部分的(partial)、有
类型的(typed)和无类型的(untyped)。在C++中,用继承实现的多态属于完全、
有类型多态(Total Typed Polymorphism),其特点是:具有多态的各对象之间有类
型上的关系(继承关系),并且所有的方法必须同时多态(子类必须继承超类所有的
方法)。而部分多态则是指“多态对象之间不必所有方法签名都相同”,无类型多态
是指“多态对象之间不必有类型上的关系”。

现在回过头来看GP。C++是一种强类型语言,在C++中要如何实现部分多态和无类型多
态?很明显,借助于模板。但是,在弱类型语言里面根本没有这样的束缚。比如Python
,如果两个类之间有继承关系,它们天生就有完全、有类型多态关系;如果两个类之
间没有继承关系,但是两个类有一个方法的签名(signature)是相同的,而client
要的就是这个方法,那么对于这个client来说,这两个对象也是可以多态的,这就是
部分、无类型多态(Partial Untyped Polymorphism)。对于弱类型语言来说,这两
种多态都是天经地义的。

OK,现在可以回到我们的问题了。你还认为C++为GP提供的舞台非常好吗? 
 
      weikeming (2002-4-10 16:11:33)  

上班时间不准看贴子!
老老实实写代码去吧,
小心月底工资. 
 
      yanxy (2002-4-10 15:27:56)  

我的水平很低,有些地方还看不懂,但是我想作者说的没错,我们不应该看到了一样
新技术就完全丢掉老的技术,
有时候积累比创新更富有生产性 
 
      N_N_N (2002-4-10 15:05:12)  

Template 完全依赖编译器的,慢与个头大是必然的,
GP 听着很优秀,在多大的工程中才能保证编成上的便利同时也是运行速度的下降是
值得的,
C++ 优秀在于底成的控制,而GP模糊了它


 
      anrxhzh (2002-4-10 14:24:15)  

完全不同意kaku_you的观点,模板和宏的本质区别就是模板被纳入到C++的类型体系
之中。至少到目前为止,还没有那种编程语言比C++更出色地给GP提供了表演的舞台
。 
 
      anrxhzh (2002-4-10 14:16:44)  

编译器对参数类型的展开名过于冗长属于实现细节上的问题,不能提高到设计层面上
来讨论。

关于迭代子,显式的循环是“冗长乏味和容易出错的”一点都没有错,在那个例子中
,使用隐式循环,而把某些逻辑封装到额外的函数对象中,究竟是提高了设计的灵活
性还是增加了设计的复杂度,则是个见仁见智的问题,也许不能一概而论。话说回来
,没有人规定一定不能使用显式的循环,如果你有足够的自信,Just Use It. 
 
      optimizer (2002-4-10 14:08:35)  

很不错的文章,感谢译者。 荣耀 
 
      kaku_you (2002-4-10 13:57:40)  

作者的话我是非常同意的。
我一直认为模板是c++技术走火入魔的一个最大特征。发明和使用模板的人都期望使
用c++的那种低等级编译器(不是贬义)来"模拟"出范型供用户使用,结果就是利用
template这个被加了功能的"宏"变换生成了各种没有任何保护的代码。我觉得范型这
种类型理所当然是给技术一般的开发人员使用的,可以使他们不必花精力在实现各种
特殊技术要求的编码上,而集中于使用类这种逻辑化了的内存数据。可惜范型的底层
实现和代码保护本来应该是很复杂的,而单纯的c++的编译器只能在外观上晃花一下
程序员的眼睛,动态运行时的复杂情况就只好听天由命了。 
 
      redpower (2002-4-10 12:49:25)  

good very good 
 
      classfactory (2002-4-10 12:20:06)  

Nobody forces you to use templates. Template is an implementation of GP, 
but GP does not equal template!

Anyway, C++ is leaded by the academic community (compared to C#, Basic), 
so it's not strange that an average programmer cannot grasp it easily.


 
      babysloth (2002-4-10 12:00:20)  

nod
各人有各人的需要,也有各自的看法,不必强求一致。
任何事情都有两面性,不能以为模板可以解决所有问题,也不能由此而轻视模板技术


 
      gigix (2002-4-10 10:25:12)  

还没看后面部分,只看了前两段。记得《人月神话》里面就讲过:No silver bullet
——没有任何技术可以将软件开发效率提高一个数量级。也就是说,任何的进步都是
渐变而非突变。任何一种新技术,要想站稳脚跟并发挥功用,这个时间是要以十年计
的。反观半个世纪以来技术的发展,莫不如此。

关注新技术、学习新技术、为新技术而痴迷,都是好的,都是应该的。唯一不应该的
,就是把某种新技术当作silver bullet,然后引发一场又一场的论战。 
 

--
※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: 天外飞仙]
[百宝箱] [返回首页] [上级目录] [根目录] [返回顶部] [刷新] [返回]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:5.061毫秒