[转载]被误解的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那肮脏的屁股。不过,这将会是另外一个故事了… 

展开阅读全文

误解C++——C(++)

06-18

C(++)rn今天所要探讨的,是C++中最大的误解:C++的使用。对C++使用的误解是大量其他误解的根源。为此,我找了个真实的案例,来展示C++的两种不同使用方法。相信诸位看完之后,便会明白这层误解的巨大影响。rn我差不多可以算是一个C出身的C++程序员。尽管前后也就用了一年多点的C,然后就转向了C++。但是,在长达9年的时间里,我实际上还是在用C编程,在C++里用C编程。rn大约一年多以前,一个偶然的机会,我拜读了D&E(以及其他C++大师的著作)。然后对自己的C++使用进行了反思。此后,我便认真地按照大师们的教诲,编写我的代码。正巧,我过去编写的一个软件需要升级重写。我也正好利用这个机会全面地实践一下真正的C++编程。我节选了其中的一个组件,作为案例,展示不同时代的C++。rn这个组件的功能很简单,将一组二进制序列转换成N进制的字符串,以及反过来转换。当时的需求是转换成32进制,这样26个字母加上10个数字,扣掉容易混淆的字符,正好可以有32个。但是为了将来的扩展,我还是把它做成了任意进制和字符集的转换,(只要有足够的字符可用)。rn我选择了一个比较通用的算法:把二进制序列看作是一个“超级整数”,用进制数N反复除这个“超级整数”,直到所得的商为0。每次除得到的余数,构成一个序列,便是编码的序列,但在使用前还需要将其反序。然后再用序列的每一个数字在字符表中索引,获得最后的编码字符串。解码的操作是编码的逆操作,只需把这个过程反过来,除法变乘法即可。为了节省篇幅,我只考察编码,毕竟我们的目的还是比较C++不同的用法,而不是研究编码问题。rn让我们先完整地看我早先的做法,然后再看后来的做法。rn首先,我编写了一个函数,LongDiv(),用来执行“超级整数”的除法:rnunsigned char LongDiv(unsigned char* dividend, int n, unsigned char divisor, unsigned char* result, unsigned char *eigenvalue=NULL)rnrn if(dividend==NULL || result==NULL || n < 0)rn return -1;rnrn long op=0;rn unsigned char m=0;rn unsigned char e=0;rnrn for(int i=(n-1); i>=0; i--)rn rn op+=dividend[i];rn result[i]=op/divisor;rn m=op%divisor;rn op=m;rn op<<=(szT1*8);rn e|=result[i];rn rnrn if(eigenvalue!=NULL)rn *eigenvalue=e;rnrn return m;rnrn被除数是一个序列,用指针dividend传递,序列的个数用n传递,divisor是除数,结果放在指针result所指定的内存中,eigenvalue是指向特征值的指针,函数返回余数。rn我之所以没有重载操作符/是为了能够在除的同时,获得余数和一个特征值。这个特征值表明了在除的过程中,二进制序列的每个数值的商是否都为0。如果都为0,整个序列的商就为0,编码也就完成了。rn然后,我编写了编码的核心函数:rnint EncodeAll(int N, unsigned char* data, int ndata, unsigned char* sqs)rnrn if(data==NULL || ndata<0)rn return -1;rnrn DataT mx=-1;rn double m=mx; m++;rn double dres=(ndata*::log(m))/::log((double)N);rn int nres=dres, ne=log(m)/::log((double)N);rn nres+=((dres-nres)==0) ? 0 : 1;rnrn if(sqs==NULL)rn return nres;rn rn DataT e=1;rn int i=0;rnrn for(; e!=0;)rn rn sqs[i]=::LongDiv(data, ndata, N, data, &e);rn i++;rn rnrn Reverse(sqs, i, sqs);rnrn return i;rnrn参数N表示进制数,data是指向二进制序列的指针,ndata是二进制序列的长度,sqs是指向用于存放结果的数据缓存区的指针。rn这个函数的核心算法非常简单,只有6行,从for开始,到Reverse()调用。前面那一大堆恼人的东西,纯粹是一组“安全围栏”。这里需要仔细分析,让我们一段段来。rn if(data==NULL || ndata<0)rn return -1;rn这是传入参数的有效性检验,为了防止无效的引用。但是这种检验并不总是有效,因为传输的参数完全可以是一个非NULL值,但却是一个无效的引用(象Win32中著名的0xcccccccc)。这种检验的有效性依赖于整体编程中的一个习惯,就是将所有的指针,在声明后立刻赋NULL。我通常都这样做,安全第一。rn DataT mx=-1;rn double m=mx; m++;rn double dres=(ndata*::log(m))/::log((double)N);rn int nres=dres, ne=log(m)/::log((double)N);rn nres+=((dres-nres)==0) ? 0 : 1;rn这是我最痛恨的一段代码,目的是为了计算出一个长度为ndata的序列,编码后需要多长的数据缓冲区。(这段代码让我把中学代数的指数部分好好地复习了一遍)。rn if(sqs==NULL)rn return nres;rn如果sqs指针的值为NULL(空指针),那么返回所需的数据缓冲区的长度,不执行具体操作。否则,执行数据编码操作。这个手法是从一些C API库中学来的,为了让算法能够适应变长的数组。过会儿我们会看到这个手法是如何工作的。rn下一步,需要将编码后的数字序列转换成字符。所以,我作了一个字符表:rnclass CCharMaprnrnpublic:rn CCharMap(const char *CharSet)rn rn if(CharSet==NULL)rn rn m_nDtoC=0;rn m_aDtoC=NULL;rn rn elsern rn m_nDtoC=::strlen(CharSet);rn m_aDtoC=new char[m_nDtoC+1];rn ::memcpy(m_aDtoC, CharSet, m_nDtoC);rn m_aDtoC[m_nDtoC]='\0';rn rn ::memset(m_aCtoD, 0, ASSICSETSIZE*sizeof(int));rn InitCharMap();rn rnrn ~CCharMap()rn rn delete m_aDtoC;rn rnrnprotected:rn char *m_aDtoC;rn int m_nDtoC;rn int m_aCtoD[ASSICSETSIZE];rnrnprotected:rn void InitCharMap()rn rn for(int i=0; im_nDtoC || data<0 || m_aDtoC==0)rn return '\0';rnrn return m_aDtoC[data];rn rnrn int Char2Data(char chr)rn rn return m_aCtoD[chr];rn rn;rn从数值到字符的转换,我利用了一个动态分配的数组m_aDtoC(严格地讲,它不是数组)。而相反的转换,我使用一个真正的C数组m_aCtoD[],它的大小是256(#define ASSICSETSIZE 256)。所以,我的这个字符表只能应付ASSIC字符集,无法扩展。在我当时的需求情况下,这足够了。rn最后,我用一个类封装了整个编码器:rnclass CEncoder : public CCharMaprnrnpublic:rn CEncoder(const char* CharSet)rn : CCharMap(CharSet)rn rnrnpublic:rn int Data2Text(unsigned char* data, int size, char* text)rn rn if(!IsReady())rn return -1;rnrn if(text==NULL)rn return ::EncodeAll(data, size, NULL);rnrn unsigned char *pdata=new unsigned char[size];rnrn ::memcpy(pdata, data, size*sizeof(unsigned char));rnrn int n=::EncodeAll(pdata, size, text);rnrn delete pdata;rnrn for(int i=0; i 论坛

误解C++——磨刀不误砍柴工

09-10

磨刀不误砍柴工rn“磨刀不误砍柴工”这句老话用在C++身上是再合适不过了。如果把C++比喻成一把刀,那么它会是一把材质和形状都非常好的刀——只是没有开锋。所以我们要“磨刀”。rnC++这把刀材质坚硬,强度也高,或许还进行过表面处理。那自然很难磨,费时费力。不过,一旦磨好,便锋利无比,持久耐用。这还是值得的。rnC++的“磨刀”实际上就是开发库,各种可能的库,从基础库开始,到各类应用库。库越多,刀磨得越快。当然了,开发库是有代价的。需要花时间,花精力,以及无限的耐心。rn此时,我们便需要做一些估计和四则运算,以便选择如何磨这把刀。rn最关键的因素,是某件工作被重复的次数,或者近似的工作的数量。某件工作被重复的次数的含义很明显,如果一再重复自己已经做过的事,明显是愚蠢的行为。“copy-paste神功”利用源代码的可复制性,很容易地避免了重复编码。但是,这也只是稍稍“不那么愚蠢”而已。rn当这些被重复的代码发生变化,那么,每一处paste的地方都需要被修改或替换。于是,聪明的人们发明了子程序、函数、类、继承、多态、模板等等五花八门的技术手段。目的便是消除这种“愚蠢”或“不那么愚蠢”的做法。一旦某件工作被做成子程序、函数、类、模板等,实际上便形成了一个库,只是库的应用范围有所差异而已。rn相比之下,近似的工作的含义则复杂、含混得多。我们编码时,时常会发现某些工作具有不同程度的相似性。比如,我们写一个排序算法,用于int类型;下次写同样的排序算法,用于double类型;…。有多少需要排序的类型,就要写多少次算法。这些算法并非完全相同(在类型上有所差异),但其结构完全一样。由此,我们可以用一个泛型算法实现所有类型的排序(暂不考虑性能问题和类型concept需求)。rn当然,并非所有的代码都具有如此高的相似性。代码的相似性越少,创建抽象的库代码的难度越大。所以,库是有限度的。综合考虑创建库的代价和效用,便可以指导我们是否建立库,或者如何建立库。rn对于完全一模一样的代码重复,自不必说,只管做成库代码。因为做这些库代码的工作量,只比编写一次代码的工作量多那么一点。修改也是如此。rn而对于相似的代码,情况则复杂得多。一般而言,如果这些相似代码仅有少量的出现,比如3、4处,通常没有必要创建相应的库代码。特别是这些相似代码的相似程度较小,或者代码复杂的时候。此时,创建库的代价很大,但获得的收益也仅有这么3、4处而已。rn但必须指出的是,我们在考虑是否创建库时,还必须认真地考虑其他项目,或者未来项目中应用的可能性。如果是某个非常常用的功能,尽管在当前项目中只出现一次,考虑到未来其他项目的应用,也应当将其开发成库。rn回到砍柴的比喻。一把没有开封的刀,在一定程度上也能砍下一些树枝,只是砍起来费劲些,也无法砍下较粗的树枝。如果我只需要砍那么几根枝丫,不需要很多,而且以后也不会再去砍柴。那么,一把钝刀也够用了。在这种情况下,如果还费劲地磨刀,着实是一种浪费。相反,如果我今天要砍一整担柴火,或者需要日复一日地砍柴。那么,我最好还是把刀磨磨好再说。rn磨刀也有难有易。材质坚硬(俗称“钢火”好)的刀,磨起来费力。但更锋利,更耐用。材质较软的刀,尽管磨起来快。但要使其锋利和耐用,比较困难。(因为材质软的刀,在磨到一定程度后,刃口会向上卷起。如果再反过来磨,又会向反方向卷)。rnC++就属于那种材质坚硬的刀。(而且生产厂家出于成本考虑,也没有为其开锋)。于是,作为“职业砍柴人”,有必要好好地磨砺一下这把好刀。(当然啦,也有很多“职业砍柴人”转而使用那些容易磨,或者出厂时已经开锋的“软质刀”)。rn磨刀也是有讲究的。(呵呵,我自认为在磨刀方面还是有那么一两手的)。越是硬的刀,越是不能急,一般需要循序渐进。为了不耽误柴火的产量,只能磨一点,用一点。一开始先用大角度,在锋口上磨出快口。尽管大角度的锋口不如小角度的来的锋利,但要比没开锋来得好。更重要的是,大角度锋口所花的时间要比小角度的少很多。由于我手中的是一把好刀,在砍柴的过程中,基本上不会有什么损耗。等到第二天,我再以小角度磨刀。同样,也不打算在第二天就全部搞定,继续用磨了一半的刀砍柴。经过第二天的磨砺,刀会比第一天好用些。然后第三天同第二天一样。以此类推,直到若干天后,刀完全磨好。此后,只需定期打磨一下,维持刀具的锋利即可。相比之下,那些软质的刀则需要更频繁地磨,以维持锋利程度。rn好了,刀就磨到这里吧。我们来看看如何“磨”C++。这里就用一个现实的案例来加以说明吧。rn现在很多应用软件,特别是MIS类软件,都需要访问数据库,然后把数据提取出来,进行进一步加工,或者直接显示在界面上。下面这样的代码,在软件中想必是随处可见的:rnvoid OnQueryClicked()rnrn DataConnection dc_(…);rn Rowset rs_(dc_, “select … from …”);rn int j(0);rnrn m_resGrid.Clear();rn m_resGrid.SetColNumber(rs_.ColNumber());rnrn while(rs_.MoveNect())rn rn for(int i=0; i 论坛

误解C++——软件工程

06-01

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

没有更多推荐了,返回首页