被误解的C++
传统上认为,C++相对于目前一些新潮的语言,如Java、C#,优势在于程序的运行性能。这种观念并不完全。如果一个人深信这一点,那么说明他并没有充分了解和理解C++和那个某某语言。同时,持有这种观念的人,通常也是受到了某种误导(罪魁祸首当然就是那些财大气粗的公司)。对于这些公司而言,他们隐藏了C++同某某语言间的核心差别,而把现在多数程序员不太关心的差别,也就是性能,加以强化。因为随着cpu性能的快速提升,性能问题已不为人们所关心。这叫“李代桃僵”。很多涉世不深的程序员,也就相信了他们。于是,大公司们的阴谋也就得逞了。
这个文章系列里,我将竭尽所能,利用一些现实的案例,来戳破这种谎言,还世道一个清白。但愿我的努力不会白费。
软件工程
一般认为,使用Java或C#的开发成本比C++低。但是,如果你能够充分分析C++和这些语言的差别,会发现这句话的成立是有条件的。这个条件就是:软件规模和复杂度都比较小。如果不超过3万行有效代码(不包括生成器产生的代码),这句话基本上还能成立。否则,随着代码量和复杂度的增加,C++的优势将会越来越明显。
造成这种差别的就是C++的软件工程性。在Java和C#大谈软件工程的时候,C++实际上已经悄悄地将软件工程性提升到一个前所未有的高度。这一点被多数人忽视,并且被大公司竭力掩盖。
语言在软件工程上的好坏,依赖于语言的抽象能力。从面向过程到面向对象,语言的抽象能力有了一个质的飞跃。但在实践中,人们发现面向对象无法解决所有软件工程中的问题。于是,精英们逐步引入、并拓展泛型编程,解决更高层次的软件工程问题。(实际上,面向对象和泛型编程的起源都可以追溯到1967年,但由于泛型编程更抽象,所以应用远远落后于面向对象)。
一个偶然的机会,我突发奇想,试图将货币强类型化,使得货币类型可以采用普通的算术表达式计算,而无需关心汇率换算的问题。具体的内容我已经写成文章,放在blog里:http://blog.csdn.net/longshanks/archive/2007/05/30/1631391.aspx。(CSDN的论坛似乎对大文章有些消化不良)。下面我只是简单地描述一下问题,重点还在探讨语言能力间的差异。
当时我面临的问题是:假设有四种货币:RMB、USD、UKP、JPD。我希望能够这样计算他们:
RMB rmb_(1000);
USD usd_;
UKP ukp_;
JPD jpd_(2000);
usd_=rmb_; //赋值操作,隐含了汇率转换。usd_实际值应该是1000/7.68=130.21
rmb_=rmb_*2.5;//单价乘上数量。
ukp_=usd_*3.7;//单价乘上数量,赋值给英镑。隐含汇率转换。
double n=jpd_/(usd_-ukp_);//利用差价计算数量。三种货币参与,隐含汇率转换。
而传统上,我们通常用一个double或者currency类型表示所有货币。于是,当不同币种参与运算时,必须进行显式的汇率转换:
double rmb_(100), usd_(0), ukp_(0), jpn_(2000);
usd_=rmb_*usd_rmb_rate;
ukp_=(usd_*usd_ukp_rate)*3.7;
double n=jpd_/((usd_*usd_jpd_rate)-(ukp_*ukp_jpd_rate))
很显然,强类型化后,代码简洁的多。并且可以利用重载或特化,直接给出与货币相关的辅助信息,如货币符号等(这点我没有做,但加上也不复杂)。
在C++中,我利用模板、操作符重载,以及操作符函数模板等技术,很快开发出这个货币体系:
template<int CurrType>
class Currency
{
public:
Currency<CurrType>& operator=(count Currency<ct2>& v) {
…
}
public:
double _val;
…
};
template<int ty, int tp>
inline bool operator==(currency<ty>& c1, const currency<tp>& c2) {
…
}
template<int ty, int tp>
inline currency<ty>& operator+=(currency<ty>& c1, const currency<tp>& c2) {
…
}
template<int ty, int tp>
inline currency<ty> operator+(currency<ty>& c1, const currency<tp>& c2) {
…
}
…
总共不超过200行代码。(当然,一个工业强度的货币体系,需要更多的辅助类、函数等等。但基本上不会超过500行代码)。如果我需要一种货币,就先为其指定一个int类型的常量值,然后typedef一下即可:
const int CT_RMB=0; //也可以用enum
typedef Currency<CT_RMB> RMB;
const int CT_USD=1;
typedef Currency<CT_USD> USD;
const int CT_UKP=2;
typedef Currency<CT_USD> USD;
const int CT_JPD=3;
typedef Currency<CT_USD> USD;
…
每新增一种货币,只需定义一个值,然后typedef即可。而对于核心的Currency<>和操作符重载,无需做丁点改动。
之后,我试图将这个货币体系的代码移植到C#中去。根据试验的结果,我也写了一篇文章(也放在blog里:http://blog.csdn.net/longshanks/archive/2007/05/30/1631476.aspx)。我和一个同事(他是使用C#开发的,对其更熟悉),用了大半个上午,终于完成了这项工作。
令人丧气的事,上来就碰了个钉子:C#不支持=的重载。于是只能用asign<>()泛型函数代替。之后,由于C#的泛型不支持非类型泛型参数,即上面C++代码中的int CurrType模板参数的泛型对等物,以及C#不支持泛型操作符重载,整个货币系统从泛型编程模式退化成了面向对象模式。当然,在我们坚持不懈的努力下,最后终于实现了和C++中一样的代码效果(除了那个赋值操作):
assign(rmb_, ukp_);
assign(usd_, rmb_*3.7);
…
我知道,有些人会说,既然OOP可以做到,何必用GP呢?GP太复杂了。这里,我已经为这些人准备了一组统计数据:在C#代码中,我实现了3个货币,结果定义了4个类(一个基类,三个货币类);重载30个算术操作符(和C++一样,实现10个操作符,每个类都得把10个操作符重载一遍);6个类型转换操作符(从两种货币类到第三货币类的转换操作符)。
这还不是最糟的。当我增加一个货币,货币数变成4个后,数据变成了:5个类;40个算术操作符重载;12个类型转换操作符重载。
当货币数增加到10个后:11个类;100个算术操作符重载;90个类型转换操作符重载。
反观C++的实现,3个货币时:1个类模板;1个赋值操作符重载模板;10个算术操作符重载模板;外加3个const int定义,3个typedef。
10个货币时:1个类模板;1个赋值操作符重载模板;10个算术操作符重载模板;const int定义和typedef分别增加到10个。
也就是说C++版本的代码随着货币的增加,仅线性增加。而且代码行增加的系数仅是2。请注意,是代码行!不是类、函数,也不是操作符的数量。而C#版本的代码量则会以几何级数增加。几何级数!!!
这些数字的含义,我就不用多说了吧。无论是代码的数量、可维护性、可扩展性C++都远远好于C#版本。更不用说可用性了(那个assign函数用起来有多难看)。
我知道,有些人还会说:货币太特殊了,在实践中这种情况毕竟少见。没错,货币是比较特殊,但是并没有特殊到独此一家的程度。我曾经做了一个读取脚本中的图形信息,并绘图输出的简单案例,以展示OOP的一些基本概念,用于培训。但如果将其细化,可以开发出一个很不错的脚本绘图引擎。其中,我使用了组合递归、多态和动态链接,以及类工厂等技术。就是那个类工厂,由于我使用了模板,使得类工厂部分的代码减少了2/3,而且没有重复代码,更易维护。关于抽象类工厂的GP优化,Alexandrescu在其《Modren C++ design》中,有更多的案例。同样的技术,还可以推广到业务模型的类系统中,优化类工厂的代码。
如果还不满意,那么就去看看boost。boost的很多库实现了几乎不可想象的功能,比如lambda表达式、BGL的命名参数等等。它为我们很多优化软件代码新思路,很多技术和方法可以促进我们大幅优化代码,降低开发成本。
最后,如果你认为C#的最大的优势在于.net平台,那我可以告诉你,这个世界上还有一种东西叫C++/CLI,完全可以满足.net的开发,而且更好,足以擦干净.net那肮脏的屁股。不过,这将会是另外一个故事了…