| 网站首页 | 业界新闻 | 小组 | 威客 | 人才 | 下载频道 | 博客 | 代码贴 | 在线编程 | 编程论坛
欢迎加入我们,一同切磋技术
用户名:   
 
密 码:  
共有 1296 人关注过本帖
标题:[转载]被误解的c++系列 from : CSDN author:longshanks
只看楼主 加入收藏
yuyunliuhen
Rank: 6Rank: 6
等 级:贵宾
威 望:20
帖 子:1435
专家分:0
注 册:2005-12-12
结帖率:100%
收藏
 问题点数:0 回复次数:9 
[转载]被误解的c++系列 from : CSDN author:longshanks

在论坛看到的一些精彩文章,拿来与大家一起分享。

被误解的C++——学习和使用
http://community.csdn.net/Expert/topic/5583/5583626.xml?temp=.9332544

学习和使用
C++太复杂了。我猜想你一定是在计算机屏幕前不住地点头。其实我也在使劲的点头。没错,C++着实复杂,大概是迄今为止最复杂的语言。于是,便产生了一个推论:C++难学难用。
这句话也相当准确,但并非所有时候都这样。C++的学习和使用可以分为若干个层面,这些层面由简到繁,由易到难,分别适合不同层次的学习者和使用者。
先让我们来看看C++究竟有多复杂。我大致列了个清单,包括C++的主要语言特性。基本的一些特性,象变量、条件、选择等等,都掠去了,主要集中在体现C++复杂性的方面:
1.指针。包括变量指针、函数指针等。可以计算。
2.引用。包括变量的引用、函数的引用等。
3.自由函数。
4.类。包括具体类和抽象类。
5.重载。包括函数重载和操作符重载。
6.成员数据。包括static和非static的。
7.成员函数。包括static和非static的。
8.虚函数。包括对虚函数的override。
9.继承。包括多继承、虚继承等。
10.多态。包括动多态和静多态。
11.类型转换。包括隐式的和显式的,以及臭名昭著的强制转换。
12.模板。包括类模板和函数模板。
13.模板特化。包括完全特化和部分特化。
14.非类型模板参数。
15.模板-模板参数。
我对照了C#的特性清单。C++比C#多的特性主要是:1、2、3、9的多继承、10的静多态、13、14、15。
而C#比C++多的特性主要有(这里没有考虑C++/CLI,C++/CLI补充了标准C++):
1.for each关键字。
2.as、is关键字。
3.seal关键字。
4.abstract关键字。
5.override/new关键字。
6.using关键字的一些补充用法。
7.implicit/explicit关键字。(C++有explicit关键字,但只用于构造函数)
8.interface关键字。
9.property。
10.delegate。
我们来分析一下。C++比C#所多的特性集中在编程技术方面,特别是编程模式,如由模板带来泛型编程和元编程机能。在OOP方面,C++走得更远,更彻底,主要表现在多继承和操作符重载方面。在传统的编程技术上,C++区分了对象和对象的引用(指针和引用)。而C#所有引用对象所持的都是引用。最后,C++拥有自由函数。
反过来再看C#,C#所多的都是些关键字。这些关键字都集中在OOP方面,并且都是以使编程技术易于理解为目的的。很多人(包括我在内)都希望C++也拥有C#的这些关键字(至少部分)。但与一般人的理解不同,C++标准委员会实际上是被编译器开发商所把持着,他们对引入关键字尤其神经过敏。没有办法,现实总是有缺憾的。
从这些方面可以看出,C++在编程机制上远远多于C#(Java的机制甚至更少)。对于新入行的人而言,一口气吞下这些内容,足以把他们撑死。相反,C#增加的关键字有助于初学者理解代码的含义。这些就是C#和Java比C++易于学习(易于理解)的真正原因。
但是,必须要强调的是,C#和Java中易于学习和理解的是代码,而不是这些代码背后的技术原理和背景。
我看到过的绝大多数C#代码都充满了重复代码和大量switch操作分派。如果这些程序员充分利用C#和Java的OOP机制,这些严重的代码冗余可以消除一半。(如果C#和Java具备C++那样的泛型编程能力,则另一半也可以消除。)这些程序员都是在没有充分理解语言机制和OOP技术的情况下编写软件,事倍功半。
这种情况在C++也有发生,但相对少些。这大概是因为C++足够复杂,使得学习者产生了“不彻底理解C++就学不会C++,就用不了C++”的想法。这种想法有利有弊,利在于促使学习者充分理解语言和语言背后的技术,而弊在于它吓跑了很多人。实际上,我们一会儿就会看到,C++可以同C#和Java一样,可以在不理解其中原理的情况下,仅仅按照既定规则编程。当然我们不希望这样,这是不好的做法。但鉴于现在业界
的浮躁心态,我们也就入乡随俗吧。
注意了!下面这句话是最关键的,最重要的,也是被长期忽略的:C++之所以复杂,是为了使用起来更简单。听不明白!自相矛盾!胡说!别急,且听我慢慢道来。
(限于篇幅,我这里只给出最后一部分案例代码,完整的案例在我的blog里:)
有三个容器,c1、c2、c3。容器的类型和元素的类型都未知。要求写一个算法框架,把c1里的元素同c2里的元素进行某种运算,结果放到c3里。
由于容器类型未知,必须使用所有容器公共的接口。所以,我写下了如下的C#代码:
public delegate void alg<T1, T2, R>(T1 v1, T2 v2, R r);
public static void Caculate_Contain<C1, T1, C2, T2, C3, T3>
(C1 c1, C2 c2, C3 c3, alg<T1, T2, T3> a )
where C1: IEnumerable<T1>
where C2 : IEnumerable<T2>
where C3 : IEnumerable<T3>
{
IEnumerator<T1> eai1 = c1.GetEnumerator();
IEnumerator<T2> eai2 = c2.GetEnumerator();
IEnumerator<T3> eai3 = c3.GetEnumerator();

while (eai1.MoveNext() && eai2.MoveNext() && eai3.MoveNext())
{
a(eai1.Current, eai2.Current,eai3.Current);
}
}
//使用
public static void CaculThem(int v1, int v2,int r) {
r=v1*v2;
}
Caculate_Contain(ai1, ai2, ai3, new alg<int, int, int>(CaculThem));
public static void CaculThem2(float v1, int v2,double r) {
r=v1*v2;
}
Caculate_Contain(af1, ai2, ad3, new alg<float, int, double>(CaculThem2));
我使用了一个委托,作为传递处理容器元素的算法的载体。使用时,用具体的算法创建委托的实例。但具体的算法CaculThem()必须同相应的容器元素类型一致。
下面轮到C++:
template<typename C1, typename C2, typename C3, typename Alg>
Caculate_Container(const C1& c1, const C2& c2, C3& c3, Alg a)
{
transform(c1.begin(), c1.end(), c2.begin(), c3.begin(), a);
}
//使用
template<typename T1, typename T2, typename R>
R mul_them(T1 v,T2 u) {
returnv*u;
}
Caculate_Container(ai1, ai2, ai3, mul_them<int, int, int>);
Caculate_Container(af1, ad2, ad3, mul_them<float, double, double>);
如果容器元素有所变化,C#代码必须重写算法CaculThem()。但C++不需要,由于mul_them<>()本身是个函数模板,那么只需将这个函数模板用新的类型实例化一下即可。
C++的代码相对简单些,灵活性也更高些。但这还不是全部,C++还有一个最终极的解法,不需要循环,不需要创建模板算法,不需要写操作函数:
transform(c1.begin(), c1.end(), c2.begin(), c3.begin(), _1*_2);
没看明白?我一开始也看不明白。这里用到了boost库的Lambda表达式。_1占位符对应c1的元素,_2的占位符对应c2的元素,_1*_2表示才c1的元素乘上c2的元素,其结果放在c3里。表达式可以写得更复杂,比如(_1*_2+3*_1)/(_1-_2)。Lambda表达式可以用在所有需要操作的算法中,比如我要去掉字符串中的“-”,可以这样写:
remove_if(s.begin(), s.end(), _1==’-’);
Lambda表达式基于一种叫做“模板表达式”的技术,通过操作符重载,将一个表达式一层一层地展开,构成一个解析树。然后作为一个函数对象传递给算法,算法在循环内调用函数对象,执行相应的计算。
没有比这更简单的了吧。原理是够复杂的,但我们可以完全不理睬其中复杂的原理,只管用就是了。别看只是一个小小的算法,要知道,再庞大的软件(象JSF的代码有1900万行之多)都是由这些渺小的算法构成的。C++提供的算法和简化算法使用的库几乎对所有的程序算法都有帮助,不仅仅对这种底层算法有效,在更高层次的算法作用更大。
这里我就不再给出C#的代码了,因为C#还不支持Lambda表达式,也无法模拟。如果想要的话,等C#3.0吧。
好了,应该是做小结的时候了。从上面的这些例子可以看出,在最基本的语句上,C#有时比C++简单些,因为C#提供了更多的关键字。但是,随着算法的逐步复杂,C++的抽象能力渐渐发挥作用。一旦需要建立抽象的算法和代码时,C++的泛型编程能力立刻爆发出巨大的能量。最后,我们利用boost::lambda库最大限度简化了算法的使用。更重要的,Lambda表达式的实现极其复杂,但是使用却异常简单。
这便是开头所说的:“C++之所以复杂,是为了使用起来更简单”这句话的含义。C++提供的那些复杂的机制,是为了构建库,以提供语言没有实现的功能,这些功能可以大幅简化开发工作。如标准库里的容器、算法,boost库的Lambda表达式、BGL的命名参数、智能指针等等。
也就是说,一个程序员可以仅仅学习最基本的C++编程技术,便可以利用现成的各种库开发软件。只管用,别问为什么。在这种情况下,学习和使用C++的难度同C#和Java相比没有本质的差别。但由于C++可以提供更灵活高效的库,在不少情况下,反而比C#和Java更好用。
要达到这种程度,程序员所需的训练的确会比C#和Java多一些。所需的训练主要集中在:标准库的使用;区别对象、指针和引用;指针、内存、资源的处理方法,智能指针的使用;类使用的一些特别要点(构造函数、隐式转换等等);多态的正确处理;模板的用法。另外还需要给学习者定下一些“规矩”,避免误用一些敏感的语言机制。这些“规矩”只需遵守,不要问为什么。一旦这些“规矩”成了本能的一部分(强化训练可以达到这种效果),程序员就成熟了。即便回过头使用C#或Java,也能很容易做到趋利避害,扬长避短。(要小心,这时候程序员很可能会骂人的。我是个比较斯文的人,一般不骂人,除了开车的时候和使用C#的时候&#61514;)。
这些内容只要编排得当,用法标准,学习者不需要花费很长的时间即可掌握,大概两三个月即可,如有半年的时间,便可以纯熟。这样训练出来的程序员基础非常扎实,无论将来学习什么语言或技术,都可以驾轻就熟。如果他还喜欢C++,那么可以进一步学习C++的高级机制,加入库开发者的行列。
对于自学者,也可以进行这样的训练。但必须要正确选择教材。Lippman的《essential C++》和《C++ Primer》是我所知的最好的教材。认真地遵循书中的线路,反复做好练习,就行了。入门书是很重要的,千万不要选择那些C(++)的书(大半本C,带上那么一点点C++的内容)。
这样,我们实际上是将C++的使用分成了两个层面,一个是应用层面,另一个是基础开发层面。当然,基础开发层面还可以分成应用性的基础开发,比如帐务管理系统专用的基础库;和工具库的开发,象boost之类的库。应用层面的开发人员不必了解(甚至可以不知道)C++所有的古怪特性,以及那些库的内部机制,只需学会使用即可。要达到这种要求,我相信对能够学会高等数学的人而言,是易如反掌的。
对于一个使用C++的企业,无需每个程序员熟悉所有的C++特性。有少量高手专注于企业级的库的设计和开发,而其余的程序员只需达到上面所说的基础C++的程度。这种配置可以获得非常高的开发效率。但前提是程序员接受正规的、扎实的基础C++培训,这在前面已经讲过了。
再强调一遍,“规矩”是至关重要的。因为C#和Java易学,主要得益于去掉了许多危险(但却非常有用和重要)的语言机制。在C++的教学中引入严格的“规矩”,是在保留这些危险但重要的机制的同时,使学习者避免其损害的手段。必须让初学者知道很多机制是不能碰的,因为这些机制不是给他们用的,是给那些充分了解其危害的人用的。
相比之下,在路上开车压死人要比在C++中犯错误容易得多(毕竟C++不会在马路上乱窜)。之所以我们没有天天出事故,是因为我们遵循了规则(交规和驾驶技巧)。C++编程也一样(任何编程都这样)。但有时人们可以在受控的情况下合理违反交规,比如执行任务的警车和救护车可以逆向行驶、闯红灯。显然这是有条件的,驾驶员受过特别训练,鸣笛和亮警示灯。就像在C++中,只有受过特别训练,在有保护的情况下方能使用象placement new这样的超危险机制一样。
不错,即便如此,C++的学习依然不是一个轻松的过程,所学的内容也比C#和Java的多。但是在付出的同时,还应看到收获。学习C++的收获不仅仅是可以使用Lambda表达式这样优雅的语法,而且能够真正地掌握编程技术中的核心,为将来的发展打好基础。当你能够熟练地使用标准库时,正宗的抽象思维的能力已经悄悄地潜入了你的骨髓,这是成为一名优秀的软件设计师的基本条件,谁不想要呢。
好了,我这里展示了C++易用性的冰山一角,解释了难学难用的原因,也提供了学好C++的方法。“C#和Java比C++容易学习和使用”这句话尽管不算诽谤,但也是被严重地夸大了。我希望大家能象Discovery Channel的Mythbusters(Discovery的保留节目,专门用实验的方式检验流言的真实性)那样,勇于尝试和实践,用自己的切身体会打破关于C++的流言。


[此贴子已经被作者于2007-6-28 19:22:45编辑过]

搜索更多相关主题的帖子: longshanks author CSDN 误解 
2007-06-28 19:22
yuyunliuhen
Rank: 6Rank: 6
等 级:贵宾
威 望:20
帖 子:1435
专家分:0
注 册:2005-12-12
收藏
得分:0 

被误解的C++——面向对象
http://community.csdn.net/Expert/topic/5588/5588185.xml?temp=.9477045

OOP
我时常听到这样一种论调:C++不是一种真正的OOP语言。(有时还会补上一句,C#(或Java)是真正面向对象的)。每隔几个星期就听到一次。
天晓得这种谣言是怎么出炉的。为此,我还特地找了几本关于OOP的书,温习了一下,看看是不是我学艺不精,漏掉了什么。其实这句谣言并不怎么高明,却传播甚广。我们只需对比一下两种语言的OOP机制就明白了,如果C++不是真正的OOP语言,那么肯定会缺少某些机制。
基本的类、继承、重载、虚函数等大家都有。C++多了多继承、操作符重载。C++没有接口,但抽象基类是接口的等价物。其实大家都差不多,OOP已经很成熟了,不会谁比谁多很多特性的。
我猜想,这则谣言可能来源于这样一句话:C#/Java是纯面向对象的语言(尽管它们都有了泛型,但通常还是划归纯面向对象语言),而C++是混合型的语言。对于不知情的人,混合型语言自然不如纯面向对象语言那么“面向对象”了。
这则谣言的另一层隐含的意思是纯面向对象的语言比非纯面向对象的好。这是人的本性所使。OOP革命后,人们发现OOP强大的抽象能力,大大简化了软件的分析和开发过程。但是好事总是会好过头。OOP功能强大,渐渐被神话成OOP无所不能。进而产生了只有纯面向对象才是最好的这种思想。
过去很多文章,包括我前一段发出的货币系统的案例,很好地证明了OOP的局限性。但在滥用OOP和合理使用OOP间存在着一道障碍:OOP也能把问题解决掉。(其实面向过程的结构化软件设计也能解决问题)。在货币系统案例中,我完全可以用OOP的方式构建货币系统,只要我耐心地反复重载操作符,总能完成的,毕竟货币只有这么几十种,常用的也只有这么十来种。
“能解决问题就行了”这成了很多程序员拒绝进步的最好借口。对于这一点,多数企业采取放任的态度。于是,开发效益也就无从谈起。
下面我们来看看OOP的其它问题。OOP的核心是多态和后期绑定(虚函数)。因为有了这两样法宝,OOP便可以使得类具备扩展的能力。考虑这样一组类:
class B
{
public:
virtual void fun() {
cout<<”I’m B”;
}
};
class D1 : public B
{
public:
virtual void fun() {
cout<<”I’m D1”;
}
};
class D1 : public B
{
public:
virtual void fun() {
cout<<”I’m D2”;
}
};
D1d1;
D2d2;
B*b=&d1;
b->fun();//显示”I’m D1”
b=&d2;
b->fun();//显示”I’m D2”
这是老生常谈了,这种机制使得这些类的使用者通过重定义虚函数fun()实现功能的扩展。或者说,实现原有代码的重用。
现在,我需要不止一个D1的对象,而是一组对象,那么就需要容器来存放。于是,我写了一组类,用于创建D1的各种容器:
class D1Array
{
public:
void add(const D1& v) {…}
D1& item(int index) {…}
const int count() const {…}

};
链表、栈等等都得写一遍。D2怎么办?继续写呗。
类太多了,为了少写一点类,我采用了OOP的一个标准用法:
class Array
{
public:
void add(const B* v) {…}
B*item(int index) {…}
const int count() const {…}

}
其他的容器类推。
如果我要把对象放进容器,则需要这样写:
Arraya;
a.add(&b1);
取出来比较麻烦,必须要这样:
D1* d=dynamic_cast<D1*>(a.item(0));//应该使用了dynamic_cast<>操作符,//不应该用(D*)这样的转换操作
这还不是最麻烦的,最麻烦的是:如果放进a的是D2的对象,而我不知道,以为是D1,那么事情就麻烦了:
D1* d=dynamic_cast<D1*>(a.item(0));//此时d==0
尽管我可以知道转型是否成功,但却不知道这个类型究竟是什么。要么把B所有的继承类都试一遍,要么利用type_info()获取类型信息,用switch分派。
这种情况是由于OOP的多态机制的单向性造成的:可以从继承类向基类隐式转换,但不能反过来。这种容器称为弱类型的,是很多麻烦的根源。比较好的做法是:在弱类型容器外做一个包装类(代理),把容器强类型化:
class D1Array
{
public:
void add(const D1& v) {…}
D1& item(int index) {…}
const int count() const {…}
private:
Arraym_impl;//元素存储在这里,D1Array的成员函数只负责转发
};
尽管类还是很多,但代码重复少很多了。而且,类型安全比代码类的数量更重要。
解决的办法就是引入泛型编程。用模板(C++)或泛型(Ada、Java、C#),可以轻松地解决这个问题。详情就不再多说了,C++的标准库就是一本最好的教材。
弱类型的容器并非没有用,一个至关重要的用途,就是构建异类型容器。发挥点想象力,如果我把D1和D2的实例都放进一个Array,然后顺序调用容器内元素的fun()成员,会发生什么呢?
a.add(new D1);
a.add(new D2);
for(int i=0; i<a.count(); ++i)
{
a.item(i)->fun();
}
会分别执行D1和D2的fun()。
这种技术有非常广泛的用途。可以说,绝大部分的多态应用都是以此形式出现的。
现在,让我们做些哲学思考。没有那样东西是万能的,也没有那样东西是无用的。老子说,福兮祸之所倚,福兮祸之所伏。这句话用在OOP上再恰当不过了。OOP既有强大的一面,也有虚弱的一面。同样,泛型编程也有无法解决的问题。前段时间的一个帖子《精通Template技术的高手请进!》(http://community.csdn.net/Expert/topic/5574/ 5574289.xml?temp=.9206049)就提出了一个泛型编程无法解决的问题。
说完强大的东西,我们再来看看C++中的一个小喽罗:自由函数。在Smalltalk、C#、Java这类纯OOP语言中,是不存在自由函数的。对于很多程序员而言,自由函数就是史前动物,只在博物馆里看见过。自由函数是面向过程开发的代表,是过时的象征。
纯OOP语言的热衷者通常都有一种倾向,将所有的操作和数据都塞进一个类里,不管是否必要。“嘿!”有人会说,“这可没办法,这些语言没有自由函数,不放进类也不行嘛。”不错,但是很多程序员把本该做成static的函数(而且应该集中放在一个独立的工具类里),写成了非static的。这种做法是对OOP的滥用。很可能严重破坏类的封装性。
其实,即便在纯面向对象的语言中,也应该将类的成员函数最小化,充分利用static成员函数,完成组合的操作。请看下面的例子:
class Rect
{
public:
point& left_top() {…}
point& right_bottom() {…}
double& width() {…}
double& high() {…}
const double area() const {
return m_width*m_length;
}
private:
point m_left_top;
double m_length;//长
double m_width;//宽
};
这种做法缺乏灵活性,如果修改Rect,不用左上角坐标、长和宽保存数据,而是用左上角坐标、右下角坐标来保存。那么,area函数必须修改。着增加了代码维护的工作量。
如果用一个自由函数(或者static成员)计算面积,便不会因为类内部结构的变化而需要修改了:
const double area(const Rect& rc) {
returnrc.width()*rc.high();
}
尽管成员函数area也可以利用width()和high()计算,但自由函数可以提供更大的灵活性:
const double area(const Rect& rc, double margin=0){…}
这样,我们修改了面积函数,增加了功能,但使用的代码不用改变。也无需到处寻找,并修改每个图形类(Circle、Triangle什么的)。C#没有默认参数,可以用重载解决。并且,自由函数和static函数更有利于泛型化。总之,类的接口尽量小,用自由函数或static函数执行复杂的计算和操作。
但是,自由函数和static函数还是存在那么一丁点区别,(尽管一丁点,但有时也挺重要)。这种差别主要体现在操作符重载上(操作符可以看成是一种特殊的函数)。C#里,操作符重载必须在类中,必须是static成员:
class A
{
public static A operator+(A a, A b){…}
};
如果有个类我希望它参与+操作,就需要为其重载+操作符。但是这个类不是我写的,我也无法修改,那么此时,+操作符无法重载,也就无法让这个类参与+运算。而在拥有自由函数的C++中,操作符可以是自由函数(除了类型转换和=两个操作符)。于是,可以直接重载操作符即可:
A operator+(A a, B b) {…}
B operator+(B b, A a) {…}
重载可以非常自由,任何地方都可以,只要在用之前即可。
最后,轻松轻松,让我来抨击一下C#和Java。呵呵,开个玩笑。其实这个问题在C++中也存在,只是办法有回避而已。
先看C#和Java。有下列代码:
c.s();
c.f();
请告诉我,c是什么,类型还是对象?s()和f()是否static的?得看前面的代码,c的定义,对吧?在C++中:
c.f();
C::s();
这样两个问题都清楚了,C是类(结构),s()是static的。c是对象,f()…。等等问题没有清楚,f()不定。C++的代码比C#和Java更明确。但是,这个f()是个问题。按照C++标准,f()既可能是static的,也可能是非static的。为了代码阅读的方便(为了别人,也为了自己),我从不使用c.f()这种形式访问静态函数。
这个问题的影响不仅仅局限在代码的阅读上。对于初学者,类和对象本是一对夹缠不清的概念。尽管C#和Java(包括C++的c.f()),简化了使用,但是却不利于初学者区分类和对象的概念。尽量用两个冒号吧。包括初学者在内,不要贪图享乐。
好,总结。这里我们简单地回顾了OOP的力量和不足。也提到了GP的力量和不足。也拜访了微末的自由函数。于是我们可以得出这样的结论:C++中的众多编程技术都有各自的优缺点,但它们都是互补的。只要我们充分地运用这些技术,取长补短,任何问题都不在话下。
关于C++的编程模式,Bjarne Stroustrup给出了一下总结:C++是“多模式编程语言”,支持四种编程模式:面向过程、数据抽象、面向对象和泛型编程。(我觉得应该是四个半模式,应该算上模板元编程这半个)。这些模式整合在一起,相辅相成,构成了强大的威力。任何偏费都会削弱它的价值。合理使用C++,享受多模式编程带来的愉悦!:)


Go confidently in the  directions of your dreams,live the life you have imagined!Just do it!
It is no use learning without thinking!
2007-06-28 19:24
yuyunliuhen
Rank: 6Rank: 6
等 级:贵宾
威 望:20
帖 子:1435
专家分:0
注 册:2005-12-12
收藏
得分:0 

被误解的C++——软件工程
http://community.csdn.net/Expert/topic/5574/5574581.xml?temp=9.881854E-04
被误解的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那肮脏的屁股。不过,这将会是另外一个故事了…


Go confidently in the  directions of your dreams,live the life you have imagined!Just do it!
It is no use learning without thinking!
2007-06-28 19:25
yuyunliuhen
Rank: 6Rank: 6
等 级:贵宾
威 望:20
帖 子:1435
专家分:0
注 册:2005-12-12
收藏
得分:0 

被误解的C++——类型
http://community.csdn.net/Expert/topic/5622/5622028.xml?temp=.6748163
类型
任何一种语言都有类型。对类型的不同的态度,造就了语言的个性。我们通常会将语言分为“强类型”和“弱类型”。
通常认为C++是强类型的。但也有反对意见。反对者认为,既然C++拥有隐式类型转换,那么就不该作为强类型语言。我这里不打算趟这潭混水,强类型还是弱类型,没有什么实际意义。
这里,我打算认真地考察一下C++独特的类型系统,来探寻C++在语言中特立独行的根源。我会尽可能不涉及语言的比较,至少不涉及他们的好坏,以免引发新一轮的口水仗。
强类型提供了很好的类型安全,但缺少灵活性。弱类型化后,灵活性提高了,但类型安全无法保障。C++所作的探索,就是寻找一种方式,在强类型的情况下,允许提供灵活,但又安全的类型系统。
让我们先从C++的内置类型说起。
C++的类型分为内置类型和用户定义类型。内置类型主要包括整数类型、浮点类型、引用类型(指针和引用)等等。我们先来分析一下内置类型,以整数类型为例。
我们知道,一个整数类型可以进行初始化、赋值、算术运算、比较、位操作,以及参与逻辑运算:
int a=10;//初始化
int b;
b=a;//赋值
int c=(a+b)*(a-b);//算术运算
if(c==b)…//比较
a=c&b;//位操作
if(c==b || !a)…//逻辑运算
当然,其他的还包括取地址、取引用等类型的基本操作。
这些操作都是语言赋予整数类型的基本操作,我们无需对其进行而外的转换或者处理。但是,当我们把目光转向用户定义类型后,问题就复杂化了。由于C++被定位于系统级开发语言(实际上C++什么开发领域都可以胜任,但最初发明它时是打算用于开发系统软件的),所以时常会需要一些古怪的操作,比如把一个用户定义类型赋值给int类型,这种操作在强类型语言中是不合规矩的。
如果我们不管三七二十一,把用户定义类型按位拷贝给int类型(这是int类型之间赋值操作的语义),那么准保会惹上大麻烦的。但如果在特定情况下,这种操作是需要的(当然不一定是必需的)。那么,我们就应当提供一种方法,允许这种赋值操作在受控的情况下进行。
为此,C++引入了操作符重载(学自Ada),以及一些相关的机制。通过这些机制,使我们(几乎)可以按照内置类型(如整数)的行为设计用户定义的类型。下面我通过一个案例慢慢讲述如何把一个用户类型变成内置类型的模仿者。这个案例来源于前些日子论坛上的口水仗,就是开发variant类型。为了简化问题,我选取了三种具有代表性的类型int,double,char*作为variant包容的目标,并且不考虑性能问题。
首先,我定义了一个枚举,为了使代码能够更加清晰:
enum
{
vt_empty=-1,//空variant
vt_double=0,//double类型
vt_int=1,//int类型
vt_string=2//字符串类型
};
然后,定义variant的基本结构。我使用了最传统的手法,union。
class variant
{
private:
intvar_type;//variant包含的类型标记
union
{
doubledbval;
intival;
char*csval;//由于union不能存放拥有non-trivial构造函数等成员,
// 所以只能用char*,提取数据时另行处理
};
};
现在,我们一步步使variant越来越像一个内置类型。看一下int类型的初始化方式:
int a(0), b=0;
int(0);//创建并初始化一个int临时对象
我们先来考虑用一个variant对象初始化另一个variant对象。实现这个功能,需要通过重载构造函数:
class variant
{
public:
variant(const variant& v) {…}

};
这是一个拷贝构造函数,使得我们可以用一个variant对象初始化另一个variant对象:
variant x(a), y=a; variant(a);//假设a是一个拥有有效值的variant对象
如果我们没有定义任何构造函数,那么编译器会为我们生成一个复制构造函数。但这不是我们要的,因为编译器生成的复制构造函数执行浅拷贝,它只会将一个对象按位赋值给另一个。由于variant需要管理资源引用,必须执行深拷贝,所以必须另行定义一个赋值构造函数。
按C++标准,一旦定义了一个构造函数,那么编译器将不会再生成默认构造函数。所以为了能够如下声明对象:
variant x;
我们必须定义一个默认构造函数:
class variant
{
public:
variant(): var_type(vt_empty) {…}

};
下一步,实现variant对象间的赋值。C++中内置类型的对象间赋值使用=操作符:
int a=100, b;
b=a;
用户定义的类型间的赋值也使用=操作符。所以,只需重载operator=便可实现对象间的赋值:
class variant
{
public:
variant& operator=(const variant& v) {…}

};
variant x, y;
x=y;
int是一种可以计算的数值类型。所以,我们可以对int类型的变量执行算术运算、比较、逻辑运算、位运算等:
int a、b、c、d、e、f、g;
a=b+c;
d=a-b;
e/=c;
c==d;
if(!c) …
f=f<<3;

同样,variant涵盖了几种数值类型,那么要求其能够进行这些运算,也是理所当然的:
variant a、b、c、d、e、f、g;
a=b+c;
d=a-b;
e/=c;
c==d;
if(!c) …
f=f<<3;

为实现这一点,C++提供了大量的操作符重载。在C++中,除了“.” 、“.*”、“? :”、“#”、“##”五个操作符,RTTI操作符,以及xxx_cast外,其余都能重载。操作符可以作为类的成员,也可以作为全局函数。(类型转换操作符和“=”只能作为类的成员)。通常,将操作符重载作为全局函数更灵活,同时也能避免一些问题。
我们先重载操作数都是variant的操作符:
bool operator==(const variant& v1, const variant& v2) {…}
bool operator!=( const variant& v1, const variant& v2) {…}
variant& operator+=( const variant& v1, const variant& v2) {…}
variant operator+( const variant& v1, const variant& v2) {…}

需要注意的是,对与variant而言,他可能代表了多种不同的类型。这些类型间不一定都能进行运算。所以,variant应当在运算前进行类型检查。不匹配时,应抛出运行时错误。
C++允许内置类型按一定的规则相互转换。比如:
int a=100;
double b=a;
a=b;//可以转换,但有warning
为了使variant融入C++的类型体系,我们应当允许variant同所包容的类型间相互转换。C++为我们提供了这类机制。下面我们逐步深入。
我们先处理初始化。非variant类型初始化也是通过重载构造函数:
class variant
{
public:
variant(double val) {…}
variant(int val) {…}
variant(const string& val) {…}

}
这些是所谓的“类型转换构造函数”。它们接受一个其它类型的对象作为参数,在函数体中执行特定的初始化操作。最终达到如下效果:
int a=10;
double b=23;
string c(“abc”);
variant x(a), y=b; variant(c);
接下来,处理不同类型和variant对象赋值的问题。先看向variant对象赋值。同样通过=操作符:
class variant
{
public:
variant& operator=(double v) {…}
variant& operator=(int v) {…}
variant& operator=(const string& v) {…}
variant& operator=(const char*) {…}//该重载为了处理字符串常量

};
这样,便可以如下操作:
int a=10;
double b=23;
string c(“abc”);
variant x,y,z;
x=a;
y=b;
z=c;
然后再看由variant对象向其它类型赋值。实现这种操作需要利用类型转换操作符:
class variant
{
public:
operator double() {…}
operator int() {…}
operator string() {…}

};
使用起来和内置类型赋值或初始化一样:
variant x(10), y(2.5), z(“abc”);
int a=x;
double b=y;
string c;
c=z;
现在,variant已经非常“象”内置类型了。最后只需要让variant同其它类型一起参与运算便大功告成了。我们依然需要依靠操作符重载,不过此处使用全局函数方式的操作符重载:
bool operator==(const variant& v1, int v2){…}
bool operator==(int v1, const variant& v2){…}
bool operator==(const variant& v1, double v2){…}
bool operator==(double v1, const variant& v2){…}
bool operator==(const variant& v1, const string& v2){…}
bool operator==(const string& v1, const variant& v2){…}
bool operator==(const variant& v1, const char* v2){…}
bool operator==(const char* v1, const variant& v2){…}

variant& *=(const variant& v1, double v2){…}
variant& *=(double v2, const variant& v2){…}

我们可以看到,对于每个非variant类型,操作符都成对地重载。通过交换参数的次序,实现不同的操作数类型次序:
10+x; x+10;
至此,variant已经基本完成了。variant可以象内置类型那样使用了。

必须说明的是,实现variant所用的的C++特性,有些是非常危险的角色。不恰当地使用类型转换操作符和类型转换构造函数,会让人得到意想不到的结果。比如,一个类中定义了operator int操作符:
class X
{
public:
operator int() {…}

};
尽管我们没有为X重载operator+操作符,但是我们依然可以合法的编写这样的代码:
Xa, b;
a+b;
因为编译器会很“积极”地使用这个操作符,将a和b隐式地转换成int,而int之间是可以“+”的。有时候,这可能不是我们所需要的。所以,一般情况下,尽可能不要使用类型转换操作符。用一个成员函数实现这种操作,就像std::string::c_str()那样。
如果实在需要这样做,(就像variant那样)那么,所有类型转换操作符所对应的类型上,可能用到的操作符或函数都应该重载一遍。因为编译器倾向于选择不需要执行类型转换的操作符或函数,它会优先选择为类重载的操作符或函数。
variant是一个特殊的类。它本身就是为了承载和模拟内置类型而创建的。所以,所有相关的运算操作都需要重载,这样也不会出现这种问题。但是,如果一个类没有variant那么“特殊”的话,最好还是不要使用类型转换操作符。
类似的问题也发生在类型转换构造函数上。因此,我们也应该尽量利用explicit关键字禁止编译器执行相应的隐式类型转换。所带来的不便,也仅仅局限在无法使用这个操作:
variant a=10.2;//double常量10.2不能通过explicit variant(double v)隐式
// 地转换成variant,不能进行这种初始化。
variant a(10.2);//可以使用这种形式。
所以,灵活性和安全性之间需要权衡。通常,安全性更重要。
总结一下,C++通过一系列的操作符重载和特殊构造函数,允许我们将一个类设计成如同内置类型那样的行为。这项任务的实现,完全依赖于C++巨大的灵活性。但是,灵活性是一柄双刃剑,带来好处的同时,也会带来很多问题。只有在特定的场合下,在具备一定的条件下,才能保证这些灵活性不带来伤害。
此外,操作符重载会带来大量语义问题。对于每个内置类型,操作符都有特定的语义,但是毫无顾忌的重载经常会破坏这些语义。因此,在重载操作符时,最好保持操作符原有的语义。否则,也应预先做好约定,以避免使用上的问题。而且,重载操作符还可能引发语义上的矛盾。比如string上的+已被定义成为接合字符串。那么如果variant内的实际类型是string,也应认为此时variant上的+是接合字符串。但是如果variant中的string包含的是文本化的数字,而程序员下意识地会把它作为数字处理,用+来累加这两个数字,于是便产生了问题。所以,操作符的重载必须谨慎。
variant类型本身是面向过程编程方式的产物,现今的一些语言,特别是能够很好地支持泛型编程的语言,已经基本上无需variant类型。现在variant的主要用途已集中在不同语言之间的交互接口上。在单一语言的编程中,泛型具有更好的类型安全性和更高的效率。
在编写variant的时候,如此众多的操作符重载,必然是一项艰巨的工作。在实际应用中,实打实地去写这些代码,太浪费了。(或许ms有这实力和耐心)。所以,我决定利用C++提供的强大的抽象机制,设法简化variant的实现。这里篇幅有限,下次再继续吧。



Go confidently in the  directions of your dreams,live the life you have imagined!Just do it!
It is no use learning without thinking!
2007-06-28 19:26
yuyunliuhen
Rank: 6Rank: 6
等 级:贵宾
威 望:20
帖 子:1435
专家分:0
注 册:2005-12-12
收藏
得分:0 

被误解的C++——模板和宏
http://community.csdn.net/Expert/topic/5609/5609995.xml?temp=.8633234
模板和宏
前些日子,论坛里大打口水仗的时候,有人提出这样一个论断:模板本质上是宏。于是,诸位高手为此好好辩论了一番。我原本也想加入论战,但是觉得众人的言论已经覆盖了我的想法,所以也就作罢了。
尽管没有参与讨论,但“模板究竟和宏有什么关系”这个问题,始终在我的脑海中上下翻飞。每当我能够放松下来的时候,这个问题便悄悄地浮现。(通常都是哄儿子睡下,然后舒舒服服地冲个热水澡的时候:))。
我思索了半天,决定做些实际的代码,以了解两者的差异。现在,我把试验的结果提交给大家,让众人来评判。
模板和宏是完全两个东西,这一点毋庸置疑。模板的一些功能,宏没有;宏的一些功能,模板没有。不可能谁是谁的影子。我们这里主要想要弄清的是,模板的本质究竟是不是宏。
需要明确一下,所谓“本质”的含义。这里我假定:一样东西是另一样东西的“本质”,有么后者是前者的子集,要么后者是通过前者直接或间接地实现的,要么后者的基础原理依赖于前者。如果哪位对此设定心存疑议,那么我们就得另行讨论了。
首先,我编写了一个模板,然后试图编写一个宏来实现这个模板的功能:
template<typename T>
class cls_tmpl
{
public:
string f1() {
strings=v.f()+”1000”;
return s;
}
void f2() {
v.g();
}
private:
Tv;
};
下面是宏的模拟:
#definecls_mcr(T) \
class \
{\
public:\
void f1() {\
v.f();\
}\
void f2() {\
v.g();\
}\
private:\
Tv;\
}
当我使用模板时,需要这么写:
cls_tmpl<Tp1>ct;
使用宏的版本,这么写:
cls_mcr(Tp1)cm;
两者写法一样。但是下列代码便出现问题:
cls_tmpl<Tp1>ct1;
cls_tmpl<Tp1>ct2;
ct1=ct2;//Ok,ct1和ct2是同样的类型
cls_mcr(Tp1)cm1;
cls_mcr(Tp1)cm2;
cm1=cm2;//编译错误,cm1和cm2的类型不同
由于cls_mcr(Tp1)两次展开时,各自定义了一遍类,编译器会认为他们是两个不同的类型。但模板无论实例化多少次,只要类型实参相同,就是同一个类型。
这些便说明,模板和宏具备完全不同的语义,不可能用宏直接实现模板。如果要使宏避开这些问题,必须采用两阶段方式操作:
typedef cls_mcr(Tp1)cls_mcr_Tp1_;
cls_mcr_Tp1_cm1;
cls_mcr_Tp1_cm2;
cm1=cm2;//同一个类型,可以赋值
这反倒给了我们一个提示,或许编译器可以在一个“草稿本”上把宏展开,然后通过用展开后的类名将所有用到的cls_mcr(…)替换掉。这样便实现了模板。
但事情并没有那么简单。请考虑以下代码:
class Tp1
{
public:
string f() {
return“X”;
}
};

cls_tmpl<Tp1>ct1;
ct1.f1();

cls_mcr(Tp1)cm1;//编译错误:Tp1不包含成员函数g()
cm1.f1();
尽管模板和宏的代码一样,但是编译器却给出了不同的结果。回溯到cls_tmpl和cls_mcr的定义,两者都有一个f2()成员函数访问了Tp1的成员函数g()。但是,模板的代码并没有给出任何错误,而宏却有编译错误。要解释清楚这个差异,先得了解一下C++模板的一个特殊的机制:模板中的代码只有在用到时才会被实例化。也就是说,当遇到cls_tmpl<Tp1>时,编译器并不会完全展开整个模板类。只有当访问了模板上的某个成员函数时,才会将成员函数的代码展开作语义检查。所以,当我仅仅调用f1()时,不会引发编译错误。只有在调用f2()时,才会有编译错:
ct1.f2();//编译错误,Tp1不包含成员函数g()
这种机制的目的主要是为了减少编译时间。但后来却成为了泛型编程和模板元编程中非常重要的一个机制。(最早用于traits等方面,参见《C++ Template》一书。我在模拟属性的尝试中,也使用了这种机制,很好用。)
相反,宏是直接将所有的代码同时展开,之后在编译过程中执行全面的语言检查,无论其成员函数使用与否。而模板一开始仅作语法检查,只有使用到的代码才做语义检查和实际编译。
从这一点看出,即使允许宏在“草稿本”中展开,它同模板在展开方式上也存在着巨大的差别。仅凭这一点,便可以否定“模板的本质是宏”这个论断。但是,如果我们把眼光放宽一些,是否可以这么认为:尽管模板和宏采用了完全不同的展开方式,那么如果模板中的每个成员都看作独立的宏,那么是否可以认为模板是通过一组宏,而不是一个宏,实现的呢?
让我们来看模板cls_tmpl<>的成员函数f1():
string f1() {
strings=v.f()+”1000”;
return s;
}
如果我们把f1看作一个宏, f1在需要时以宏的方式展开,然后正式编译。当然,我们首先必须将模板转换成一组宏。如果哪个编译器真是这样做的,那么可以勉强地认为这个编译器是通过宏实现模板的。(不过这种样子的“宏”,还能算宏吗?)
但是,当我们考虑另一个问题,事情就不再那么简单了。请看以下代码:
x=y;
a=b;
假设x、y、a、b都是int类型。这两行代码编译后可能会变成如下等效的汇编代码(实际上是机器码):
mov eax, y
mov x, eax
mov eax, b
mov a, eax
我们可以看到,这两行代码分别转化成两条汇编指令,所不同的是参与的内存变量。可以认为编译器把赋值的汇编码(机器码)做成一个“宏”:
#define assign(v1, v2) \
mov eax, v2\
mov v1, eax
在编译时用内存变量(的地址)替换“宏”的参数。那么这种情况下,我们是否应该认为编译器(或者说编译)的本质是宏呢?
由于C++标准没有规定用什么方式展开模板,而我们也很难知道各种编译器是如何实现模板的,也就无从得知模板是否通过宏物理实现。但是,我个人的看法是,宏和模板都是语法层面的机制。如果一定要用宏这种语法层面的机制,来解释模板的(物理)本质,那也太牵强附会了。
我觉得比较合理的解释是:如果一定要把宏和模板扯上什么“亲戚关系”,那么说宏是模板的远方大表哥比较合理。两者在技术上有一定的同源性。都是以标识符替换为基础的。但是,其他在方面,我们很难说它们有多大的相似性或者关系。宏是语法层面的机制,而模板则深入到语义层面。无论是语法、语义,还是具体的实现,都没有什么一样的地方。
至于“模板的本质是宏”这种说法的始作俑者,可能是Stroupstrup本人。最初他提出模板(当时称为类型参数)可以通过宏实现。但是不久以后,便发现他心目中的模板和宏有着天壤之别。于是,他和其他C++的创建者一起建立和发展了模板的各种机制。
故事本该就此结束,但是这个说法却越传越广。我猜想原因有可能两种。其一是为了使一些初学者理解模板的基本特征,用宏来近似地解释以下模板,使人容易理解。我曾经对一些不开窍的同僚说:“如果你实在搞不清模板,可以把它理解成象宏那样的东西。但是记住,它跟宏没关系!”很多人话只听半句。他们记住了前半句,扔掉了更重要的后半句。所以,我现在再也不说这样的话了。
另一种原因可就险恶多了。一些试图打压C++的人总是不遗余力地贬损C++的各种特性,(C++的问题我们得承认,但是总得实事求是吧),特别是那些最强大的功能。而模板则是首当其冲的。如果把模板和宏,这种丑陋的、臭名昭著的“史前活化石”联系在一起,对于打击C++的名声有莫大的帮助。(即便C++社群,也非常积极地排斥宏)。
实际上,模板的本质是不是宏,根本没有什么实际意义。即便是这样,也丝毫不会影响模板的价值。很多高级的编程机制都是建立在传统的技术之上的,比如虚函数就是利用函数指针表和间接调用实现的。从没有人拿这一点说事。
但是,很多人却对模板大做文章,想借此说明模板在本质上是落后的东西。以此欺骗世人,特别是那些懵懂的初学者。我写此文的目的,就是实在忍受不了这种指鹿为马的言论,借此反击一下。
另一方面,通过模板和宏的特性的比较,可以使我们更深入地了解和理解两种机制的特性、能力和限制。温故而知新,总会有新的收获。


Go confidently in the  directions of your dreams,live the life you have imagined!Just do it!
It is no use learning without thinking!
2007-06-28 19:27
yuyunliuhen
Rank: 6Rank: 6
等 级:贵宾
威 望:20
帖 子:1435
专家分:0
注 册:2005-12-12
收藏
得分:0 

被误解的C++——高端开发
诸位留神,我要捅马蜂窝了。
呵呵,玩笑。之所以这么说,因为我打算在这里探讨如何利用C++的特性优化高端编程。高端的编程历来是VB、Delphi、Java、C#等等语言的一亩三分地,如果我敢在这里说个不字,那点口水也能把我淹死。不过,为了C++,豁出去了,就让我一个人挨炸弹吧。&#61514;
我的案例是一个小小的,不起眼的界面问题。尽管是小小的,不起眼的问题,但它的解决为我们指出在高端编程优化的一种途径。这种构想是否可行,以及是否值得,我不清楚。因为所需的机制,还在标准委员会的讨论之中,我还无法制作完整的案例加以检验。
一切从一个ComobBox开始。
有一次,我从数据库提出一组数据,放入一个ComboBox。ComboBox的每个项目还必须和一个值绑定,这样当选中一个项目的时候,便可以得到一个id,用于数据操作。这是再常规不过的操作了。在Windows API,MFC等库中,可以为每个项目指定一个“Item Data”或者“Item Data Pointer”。前者是个long,对应类型为long或能够转化成long的数据;后者是void*,对应那些非整数类型的数据。
在.net中,技术发展了,利用了OOP的多态。ComboBox的Item是一个Object^类型,我们可以创建一个ref class,重定义ToString()方法,而这个类中依然可以保留其他数据。.net中任何类型都可以多态地赋给Object^类型。ComboBox则调用每个Item的Object::ToString()方法,获得所需的显示内容。于是,ComboBox的Item既可以提供ComboBox所需的输出,也解决了附加数据的问题:
class MyItem
{
public:
MyItem(int id, String^ name) : _id(id), _name(name) {}
public:
int_id;
String^_name;

String^ ToString() {
return name;
}
};
combobox1.Add(gcnew MyItem(10, “abc”));
这些操作我在以前已经做过无数次了。但这次不同。我不小心踩了个地雷。这次不同于以前,id不再是long,而是String^。但我还以为是long,所以提取出这个id的时候,出了大错:
MyItem^ cur=dynamic_cast<MyItem^>(combobox1.SelectedItem);
cur->…; //运行时错误,cur==nullptr
幸好我用了dynamic_cast,不然还不知怎么样呢。因为放入ComboBox的对象不是MyItem类型,所以这个转换得到的是空句柄。于是,我的程序得到了了一个难看的运行时错误。
“唉,要是强类型多好!”我一声长叹。话音未落,一个念头瞬间闯入了我的脑海,就像漆黑的夜空,划过一道明亮的闪电,照亮了每一个角落。…
呵呵,没那么夸张。但我的确想到了“强类型”。
毋庸置疑,强类型可以为我们提供很多好处。首先,强类型可以减少类型错误,避免不必要的类型转换,确保类型安全;其次,由于ADT(抽象数据类型)的引入,使得类型在拥有数据结构的同时,也描述了外在的行为特征。因此,类型具备了描述业务逻辑的能力。于是,强类型化使得我们可以利用ADT的这些能力,将业务逻辑的约束直接映射到代码中。并且,利用类型的匹配机制,维持了这些约束,使我们得以在编译时拦截诸多与业务逻辑相关的错误;最后,强类型可以参与重载,使得我们可以用统一的形式编写具有相同逻辑含义的代码,以此优化软件开发。
当我想到强类型,立刻联想到模板。我们完全可以利用模板来解决这个问题。
我考虑了三种方案,(所有三种方案,我放在blog里,以节省篇幅),最终选择了一种组合方案:
首先,需要约定业务对象类(以管理员类Admin为例)提供一个默认的显示字符串构造函数,暂且称为DefDisp()。(通常,业务对象类都应该有一个默认的显示函数,为了便于在界面上显示)。然后,定义一个默认显示组织的函数对象:
template<typename T>
struct DefaultDispOp
{
string operator()(const T& item) {
returnitem.DefDisp();
}
};
最后,将DefaultDispOp作为XComboBox第二参数的默认参数:
template<typename ItemT, typename DispOP=DefaultDispOp<ItemT> >
class XComboBox
{
...
string GetItemDispString(const ItemT& item, DispOp& op) {
returnop(item);
}
}
DefDisp()所对应的显示应该是该业务对象最常用的显示,比如对于Admin而言,可能就是全名。如果只需要用默认方式显示的,那么只需简单地用业务对象类实例化XComboBox即可:
XComboBox<Admin>combobox1;
XComboBox<Admin>combobox2;
而对于有特殊显示要求的,再另外提供转换函数对象。
struct AdminDisp1
{
string operator()(const Admin& admin) {
…;
}
};
//用Admin和AdminDisp1实例化XComboBox
XComboBox<Admin, AdminDisp1>combobox1;
三种方案的分析可以看出,模板和泛型编程过度使用,反而不会达到最好的效果。通常,深度的泛型编程(包括元编程)都是在“迫不得已”的情况下使用的。比如,不使用GP,便会造成巨大的开发工作量(就像货币系统那个案例那样),或者根本无法实现(如标准库中的通用算法)。一般情况下,能使用传统技术,优先考虑传统技术,或者简单的泛型技术。
作为一个好事者(我总是一个好事者&#61514;),我不断地端详XComboBox,发现它像一个东西:容器。没错,ComboBox原本就是一个容器,只是功能更多些而已。既然是个容器,我是否可以把它用在标准算法上呢?当然可以。我们可以把它看作是一个vector或者dequeue或者list,也可以象操作序列容器那样操作XComboBox,只需为XComboBox增加一组成员和typedef:
template<typename ItemT, typename DispOP>
class XComboBox
{
public:

Iterator begin(){…}
Iterator end() {…}
void push_back(const ItemT& item) {…}
void pop_back() {…}

};
这样,我们便可以将一个容器中的业务对象直接copy到XComboBox中去:
XComboBox<Admin> combo1;
vector<Admin>vAdmins;
…//初始化vAdmins,比如来源于某种对象数据系统
copy(vAdmins.begin(), vAdmins.end(), back_inserter(combo1));
更进一步,如果我们使用的数据库结果集也兼容标准库容器。那么,可以用transform算法直接将一个结果集传递到XComboBox中:
struct RS_to_Admin
{
Admin operator()(const STLRecordSet::RowT& row) {
Admina;
…//用结果集的行初始化业务对象
returna;
}
};
XComboBox<Admin>combo1;
STLRecordSet rs=QueryData(“…”);
tramsform(rs.begin(), rs.end(), back_inserter(combo1), RS_to_Admin());
这些也表明,一旦所有的应用组件,包括数据访问、界面元素等等,都STL化之后,整个应用系统的开发便可以大幅度的简化。在这方面,走在前面的是Mathew Wilson(他的《Imperfect C++》想必很熟吧)。Wilson所开发的STLSoft以及相关的STLxxx系列库为诸多系统包括Unix、MFC、ATL、Internet、COM、.net提供了STL化的包装层。使得标准算法得以发挥最大的作用。

XComboBox的技术可以进一步推广到其他界面组件上,比如ListBox、TreeView等。但是一旦试图将复杂的界面组件,如ListView、Grid等,强类型化,便会遇到很大的问题。
ListView和Grid等组件,都包含一组,而不是一个列。如果我们试图让每一个列强类型化,那么势必要求模板具备个数不定的模板参数:
XListView<int, string, Admin, float>admin_list;
XGridView<string, double, string, Product, Deptmant>product_grid;
从技术上讲,这是一种Tuple。boost的tuple可以为我们提供一些思路。但是,boost的tuple仅仅是模拟了可变模板参数的构造,而并非真正的可变模板参数。真正的可变模板参数需要等到C++09标准正式通过以后才能确定。(好消息是,可变模板参数已经投票通过,进入标准草案)。
假设我们已经拥有了可变模板参数这种机制,那么我们可以通过元函数(meta-function)提取相关的数据。比如当我们试图获取admin_list的第三列,那么可以这样写:
Column<Admin>&col_admin=admin_list.GetColumn<3>();
如果试图访问product_grid的第4列、第5行的单元格,那么可以这样:
Product prod=product_grid.GetCell<4>(5);
但是,即便我们获得了可变模板,还会面临诸多问题。我目前能够想到的主要包括这么一些困难:
1.显示数据转换。同ComboBox一样,ListView也会有显示转换的问题。但更复杂,因为牵涉到的不再是一个类型,而是一组类型,每个类型都有可能需要特殊的数据显示转换。
2.数据访问。数据访问的问题不仅仅在于GetCell<4>(5)这种古怪的用法,而且已经破坏了Grid的对称性特征。如果Grid行列交换(行固定而列变化),或者不存在行列差异的表格,强类型化会非常复杂。更重要的,形如GetColumn<3>()的列选择是编译期机制,如果存在运行时的列选择需求,强类型组件的实现将会出奇的麻烦。
3.命名列。由于目前模板的非类型模板参数不能是字符串,因此,我们无法通过命名列的名称执行列选择:GetColumn<”UserName”>()。未来的标准中是否会放宽对非类型模板参数的限制,目前不得而知。
从这些方面来看,对于ListView、GridView之类表式的界面组件,实现强类型化是不切实际的。但是,或许通过深入地研究和尝试,找到某种方法解决这些问题,也未可知。所以,我们最好还是让希望之门敞开,并努力搜寻可能的解决之道。
必须指出的是,虽然表式组件的强类型化问题很多,但同样的技术却可以在数据库访问中得到应用。由于结果集不存在Grid的那些数据访问问题,所以将结果集强类型化会简单得多。结果集可以看作是一种Tuple。因此,可以在结果集模板中,通过指定模板参数确定每个列的类型,并且可以进一步静态地设定字段绑定的方式,提高软件的可靠性。
现在可以总结一下了。为了应对弱类型的ComboBox容易引发的错误,我尝试着将其强类型化,以获得最佳的类型安全。之后,我扩展了ComboBox,使其得以兼容标准算法,依靠标准算法,简化不同组件间的数据交换。
最后,我试图将强类型组件的思路推广到ListView、GridView等表式组件上,但遇到了诸多棘手的问题。在这些问题解决之前,(某些问题还依赖于C++09标准),我无法肯定强类型组件可以全面实现。但无论如何,一些简单形式的强类型组件,如XComboBox,完全可以实现,并帮助我们解决编程中的问题,简化软件的开发。



Go confidently in the  directions of your dreams,live the life you have imagined!Just do it!
It is no use learning without thinking!
2007-06-28 19:28
aipb2007
Rank: 8Rank: 8
来 自:CQU
等 级:贵宾
威 望:40
帖 子:2879
专家分:7
注 册:2007-3-18
收藏
得分:0 
看过了!呵呵~

Fight  to win  or  die...
2007-06-28 19:28
yuyunliuhen
Rank: 6Rank: 6
等 级:贵宾
威 望:20
帖 子:1435
专家分:0
注 册:2005-12-12
收藏
得分:0 

被误解的C++——优化variant实现
http://community.csdn.net/Expert/topic/5626/5626229.xml?temp=.2273828
优化variant实现
上一次,我大概制作了一个variant类型,并设法赋予这个类型同C++内置类型几乎一样的行为。但是,具体实现起来,倒是有点望而生畏。想想看,如果我的variant需要包容5种类型,那么单单一个操作符,就需要5×5+1=26个操作符重载(那单独一个是variant类型操作数的重载)。所有二元操作符都是如此。
通过蛮力来实现variant,尽管可能,但着实愚蠢。我们必须寻找更简单有效的实现途径,避免为了一个“屁眼大的”variant(请原谅我说粗话)写上几万行代码,而且这些代码就像一窝小猪仔那样相像。好在C++为我们提供了充足的现代武器,使我们拥有足够的火力摆平这些问题。
让我们先从操作数都是variant的二元操作符入手:
variant operator+( const variant& v1, const variant& v2) {…}

简单起见,先考察operator+的实现,然后扩展到其他操作符。
由于操作数是variant类型,那么它们可能代表不同的类型。我们必须知道操作数的实际类型,才能对其实施相应的+操作。最传统的办法就是使用switch:
variant operator+(const variant& v1, const variant& v2) {
switch(v1.get_type_code())
{
case vt_double:
switch(v2.get_type_code())
{
case vt_double:
…;
break;

}
case vt_int:
switch(v2.get_type_code())
{
case vt_double:
…;
break;

}

}
}
好家伙,又是一个组合爆炸。一步步来,我们先来处理这堆讨人嫌的switch…case…。一般而言,对于一个函数(操作符)内的的大量分派操作,可以使用包含函数指针的数组或者容器替代。如果标记值(这里的vt_...)是连续的,可以直接使用数组;如果标记值不连续,可以使用关联容器。这里vt_...是连续的,所以用数组比较方便:
typedef variant (*add_op_t)(const variant& v1, const variant& v2);
add_op_t tbl_type_ops[3][3];//函数指针表,假设variant对应三种类型
variant add_op_double_double(const variant& v1, const variant& v2){…}
variant add_op_double_int(const variant& v1, const variant& v2){…}

variant add_op_int_double(const variant& v1, const variant& v2){…}

tbl_type_ops [vt_double][vt_double]=add_op_double_double;
tbl_type_ops [vt_double][vt_int]=add_op_double_int;

variant operator+(const variant& v1, const variant& v2) {
returntbl_type_ops [v1.get_type_code()][v2.get_type_code](v1, v2);
}
operator+的代码是简单了,但是它的代码实际上转嫁到每个专用操作函数add_op_...上去了。并没有简化多少。下一步,我们来处理这些add_op_...:
template<typename VT1, typename VT2>
variant add_op(const variant& v1, const variant&v2) {
throwexception(string(“cannot add type ”)+typeid(VT1).typename()
+”to”+typeid(VT2).typename());
}//主函数模板,对应不兼容类型的操作。抛出异常。
template<>
variant<double, double> add_op(const variant& v1, const variant&v2) {
returnvariant(v1.dbval+v2.dbval);
}//针对double+double的操作

tbl_type_ops [vt_double][vt_double]=add_op<double, double>;
tbl_type_ops [vt_double][vt_int]=add_op<double,int>;

利用函数模板,及其特化,消化掉一部分的冗余代码。利用主函数模板实现所有不能互操作的类型操作,而可操作的类型则使用特化的模板实现。当然,冗余代码还是存在,这部分我们一会儿再处理。先来看看tbl_type_ops的填充。这部分代码也存在组合爆炸。为消除这个问题,我请出了模板元编程(TMP)。当然,我没有那么好的本事去直接倒腾TMP,我“借用”了boost::mpl::vector来实现这步优化:
//使用mpl::vector存放variant包容的类型
typedef boost::mpl::vector<double, int, string>op_types;
const int n_types=boost::mpl::size<op_types>::value;
//操作函数指针表
typedef variant (*add_op_t)(const variant& v1, const variant& v2);
add_op_t tbl_type_ops[n_types][n_types];
//填充函数指针表单个元素
template<int m, int n>
inline void set_tbl_type() {
typedefmpl::deref<mpl::advance<mpl::begin<op_types>::type,
mpl::int_<m> >::type>::typetype_1;
typedefmpl::deref<mpl::advance<mpl::begin<op_types>::type,
mpl::int_<n> >::type>::typetype_2;

tbl_type_ops [m][n]=add_op<type_1, type_2>;
}
//填充函数指针表单元的函数对象类
template<int m, int n>
struct fill_tbl_types_n
{
void operator()() {
set_tbl_type<m-1, n-1>();//填充函数指针单元
fill_tbl_types_n<m, n-1>()();//递归
}
};
template<int m>
struct fill_tbl_types_n<m, 0>//特化,递归结束
{
void operator()() {}
};
//填充函数指针表行的函数对象类
template<int m, int n>
struct fill_tbl_types_m
{
void operator()() {
fill_tbl_types_n<m, n>()();//创建并调用fill_tbl_types_n函数对象
fill_tbl_types_m<m-1, n>()();//递归
}
};
template<int n>
struct fill_tbl_types_m<0, n>//特化,递归结束
{
void operator()() {}
};
void fill_tbl_op() {
fill_tbl_types_m<n_types, n_types>()();
}
这里运用函数对象类模板的特化,构造了函数指针表的填充自动函数。在需要时,只需调用fill_tbl_op()函数即可。该函数中创建fill_tbl_types_m<n_types, n_types>函数对象,然后调用。这个函数对象的operator()首先创建并调用fill_tbl_types_n<m, n>函数对象。后者先调用set_tbl_type<m-1, n-1>模板函数,执行填充tbl_type_op数组的[m-1, n-1]单元格。然后递归调用fill_tbl_types_n<m, n-1>函数对象。直到n-1==0,编译器便会选择特化版本的fill_tbl_types_n<m, 0>函数对象。该特化的operator()操作符重载是空的,因此递归结束。这样完成一行的填充。然后,fill_tbl_types_m<m, n>则递归调用fill_tbl_types_m<m-1, n>函数对象,填充下一行。直到调用fill_tbl_types_m<0, n>特化版本,结束递归。
现在需要仔细看一下set_tbl_type<>函数模板。该模板上来就是两个typedef。这两个typedef创建了两个类型别名,分别用m和n做索引,从boost::mpl::vector<double, int, string>中取出相应的类型:
typedefmpl::deref<mpl::advance<mpl::begin<op_types>::type,
mpl::int_<m> >::type>::typetype_1;

头晕是吧。我的头还有点晕呢。这就是模板元编程,不停地鼓捣类型。具体的操作可以参考boost文档或《The Template Meta-programming》一书,我这里就不多说了,反正就是从一个存放类型的vector中取出所需的类型。
这样获得的两个类型用来实例化add_op<>()模板函数,并且填充到tbl_type_ops[m][n]元素中。
这样,利用TMP和GP两种强大的机制,消除了tbl_type_ops填充的组合爆炸问题。如果我们需要向variant中加入新的类型,那么只需在mpl::vector<double, int, string>中直接加入类型即可:
typedef mpl::vector<double, int, string, bool, datetime>op_types;
OK,下面回过头,来处理add_op<>中存在的组合爆炸。对于每一对可以直接或间接相加的类型,都需要做一个add_op<>的特化版本。这当然不够好。我们可以进一步抽象add_op,然后加以优化。我把整个add_op<>模板改写成如下代码:
template<typename VT1, typename VT2>
variant add_op(const variant& v1, const variant& v2) {
typedeftype_ret<VT1, VT2>::typeRetT;
returnvariant(v1.operator RetT()+v2.operator RetT());
}
这里,我首先利用type_ret模板(模板元函数)获得两个操作数相加后应有的返回类型。这个模板一会说明。然后,调用variant上的类型转换操作符,将两个操作数转换成返回类型。最后相加,并创建返回variant对象。代码非常简单,没法再简单了。
再来看看type_ret<>:
template<typename T1, typename T2>
struct type_ret
{
typedefT1type;
};
template<>
struct type_ret<int, double>
{
typedefdoubletype;
};
template<>
struct type_ret<string, double>
{
typedefdoubletype;
};
…//其他类型对的返回类型
type_ret<>是典型的模板元函数,没有任何实际代码,只有编译时计算的typedef。主模板将第一个类型参数typedef出一个别名。其后的模板特化对于一些特殊的情况做出定义,如int和double相加返回第二个操作数类型double(即所谓的类型提升)。
我们现在已经优化了variant+varint的代码。现在来看看如何优化variant类型和其他类型的加法:
template<typename T>
variant operator+(const variant& v1, const T& v2) {
returnv1+variant(v2);
}
template<typename T>
variant operator+(const T& v1, const variant& v2) {
returnvariant(v1)+v2;
}
这非常简单,直接利用了variant+variant,将其它类型的操作数转换成variant类型,然后相加。

好,加法完成了。但还有其他操作符。每个操作符都做那么一个函数指针表,也不见得高明到哪里去。现在需要整合优化这些操作符。这里,我想到了两种方法:一种是将函数指针表和填充操作整个地封装在一个模板中,模板参数采用int op形式。每一种操作符对应一个整数(或枚举值),并利用某种手段(如singleton)唯一生成一组全局的函数表,以此处理每一种操作。另一种方法是为函数指针表加一个维度(二维扩展到三维),新的维度对应不同的操作符。前一种方法灵活性强些,而且有利于性能优化;而后一种方法实现简单。这里我使用后一种方法:
enum
{
vt_op_add=0,
vt_op_add_assign=1,
vt_op_equal=2,
vt_op_not_equal=3

};
const int vt_op_num=10;

template<typename T, int op>
struct var_op;

template<typename T>
struct var_op<T, vt_op_add>
{
T operator()(const T& v1, const T& v2) {
returnv1+v1;
}
}

template<typename T>
struct var_op<T, vt_op_equal>
{
bool operator()(const T& v1, const T& v2) {
returnv1==v1;
}
}

template<typename VT1, typename VT2, int op>
variant variant_op(const variant& v1, const variant& v2) {
typedeftype_ret<VT1, VT2>::typeRetT;
returnvariant(var_op<RetT,op>()(v1.operator RetT()+v2.operator RetT()));
}
我使用了一个函数对象模板var_op<>抽象各种算法(二元)。针对每一种运算符特化。抽象的variant_op函数模板实例化var_op<>,然后调用。获得相应的操作。
观察variant_op的模板参数,会发现已经包含了一个操作的基本要素。(眼下这个形式正好符合逆波兰表达式)。
接下来,只需将函数指针数组,及其填充算法加以扩展,便可大功告成:
add_op_t tbl_type_ops[n_types][n_types][vt_op_num];
//填充函数指针表单个元素
template<int m, int n, int op>
inline void set_tbl_type() {
typedefmpl::deref<mpl::advance<mpl::begin<op_types>::type,
mpl::int_<m> >::type>::typetype_1;
typedefmpl::deref<mpl::advance<mpl::begin<op_types>::type,
mpl::int_<n> >::type>::typetype_2;

tbl_type_ops [m][n][op]=add_op<type_1, type_2, op>;
}

template<int m, int n, int op>
struct fill_tbl_types_op
{
void operator()() {
set_tbl_type<m-1, n-1, op-1>();
fill_tbl_types_op<m, n, op-1>()();//递归
}
};
template<int m, int n>
struct fill_tbl_types_op<m, n, 0>//特化,递归结束
{
void operator()(){}
}

template<int m, int n, int op>
struct fill_tbl_types_n
{
void operator()() {
fill_tbl_types_op<m, n, op>();
fill_tbl_types_n<m, n-1, op>()();//递归
}
};
template<int m, int op>
struct fill_tbl_types_n<m, 0, op>//特化,递归结束
{
void operator()() {}
};

template<int m, int n, int op>
struct fill_tbl_types_m
{
void operator()() {
fill_tbl_types_n<m, n, op>()();
fill_tbl_types_m<m-1, n, op>()();//递归
}
};
template<int n, int op>
struct fill_tbl_types_m<0, n, op>//特化,递归结束
{
void operator()() {}
};

void fill_tbl_op() {
fill_tbl_types_m<n_types, n_types, vt_op_num>()();
}

template<typename RetT, int op>
struct var_oper
{
RetT operator()(const variant& v1, const variant& v2) {
returntbl_type_ops [v1.get_type_code()][v2.get_type_code]
[op](v1, v2).operator RetT();
}
template<int op>
struct var_oper<variant, op>
{
variant operator()(const variant& v1, const variant& v2) {
returntbl_type_ops [v1.get_type_code()][v2.get_type_code]
[op](v1, v2);
}
于是操作符的实现,成了以下形式:
variant operator+(const variant& v1, const variant& v2) {
returnvar_oper<variant, vt_op_add>(v1, v2);
}
bool operator==(const variant& v1, const variant& v2) {
returnvar_oper<bool, vt_op_equal>(v1, v2);
}

如果还觉得复杂,那么可以进一步使用宏做一些包装。
好了,variant的优化基本上完成了。当然还会有一些方面值得我们去进一步地优化,比如可以利用boost的type traits和标准库的limit优化type_ret模板的实现和类型转换操作的实现等等。这里不再赘述。
需要说明的是,整个优化仅仅针对代码,并未考虑性能问题。在优化的过程中,某些手法的使用实际上降低的性能。比如函数指针表存在间接调用,不如直接使用inline函数来的高效。而且,函数指针表要求所有指向的函数必须以相同的类型返回。为了兼容+、-等操作,我使用了值返回。但对于+=等操作符完全可以利用引用返回,以提升性能。如果要解决这种问题,需要用前面提到的模板封装函数指针表的方案,为每一个操作符创建一个函数指针表加以解决。
另一个性能问题主要是在variant与其它类型的操作中,其它类型转换成variant类型然后再计算。比起直接使用目标类型计算慢不少。这个问题也可以利用GP和TMP消除,但代码会复杂不少。
理论上,利用inline和编译器优化,可以消除大部分性能问题。但不是所有的,函数指针表的间接调用,是无论如何也优化不掉的。
此外,我在实现函数指针表的构造算法时,没有使用函数模板,而是使用了函数对象模板(重载operator()的模板)。这是因为函数模板目前不能局部特化,而这里是必须的。另一方面,由于使用了递归,函数模板无法做到inline(),而使用函数对象模板则不会有此限制。表达式fill_tbl_types_m()();最终(优化)编译后的结果会是这样(伪码):
tbl_type_ops [2][2][0]=add_op<string, string, 0>;
tbl_type_ops [2][1][0]=add_op<string, int, 0>;

tbl_type_ops [1][2][0]=add_op<int, string, 0>;

递归和函数对象的调用没有了,完全inline化了。inline函数有时却无法做到这一点。而fill_tbl_types_op等模板实际上起到了代码生成器的作用。这也是GP的一个鲜为人知的功能。如果你有一大堆代码需要编写,而这些代码有很强的规律性和重复性,那么请优先考虑使用模板来为你生成代码,又快又好。
该总结了。如果审视一些代码,会发现只要存在重复和规律性,我们总能利用一些技术和方法加以优化,减少代码量,简化代码结构,减少潜在错误,最终提高开发效率。这里,我使用了C++的泛型编程和模板元编程技术,大幅优化了variant类型中的大量冗余代码。并且为variant类型构建了一个灵活,而又易于扩充的结构。此类技术有很广的应用,不仅仅局限在variant这种底层构件中。相关的一个应用就是构造抽象类工厂,在《Modren C++ Design》一书中,有很完整的案例。
此外,这类技术对于调和运行时多态(OOP)和编译时多态(GP)的矛盾有很大的作用。variant只有在运行时方能确定其具体的类型,而C++的模板只能提供编译时的GP。我利用函数指针数组(当然在更复杂的应用中,可以利用OOP的动多态机制),实现运行时分派操作。而利用GP和TMP大幅简化函数指针数组、操作实现函数,以及操作符的构造。这些技术和方法可以在大多数需要运行时多态,但又存在大量重复或雷同代码的地方得以应用。




Go confidently in the  directions of your dreams,live the life you have imagined!Just do it!
It is no use learning without thinking!
2007-06-28 19:29
yuyunliuhen
Rank: 6Rank: 6
等 级:贵宾
威 望:20
帖 子:1435
专家分:0
注 册:2005-12-12
收藏
得分:0 
回复:(aipb2007)看过了!呵呵~
嘿嘿,这么快啊
期待下一篇。

Go confidently in the  directions of your dreams,live the life you have imagined!Just do it!
It is no use learning without thinking!
2007-06-28 19:37
野比
Rank: 7Rank: 7Rank: 7
等 级:贵宾
威 望:24
帖 子:1627
专家分:516
注 册:2007-5-24
收藏
得分:0 

CSDN总是那么慢... 都没耐性去了..


女侠,约吗?
2007-06-28 19:49
快速回复:[转载]被误解的c++系列 from : CSDN author:longshanks
数据加载中...
 
   



关于我们 | 广告合作 | 编程中国 | 清除Cookies | TOP | 手机版

编程中国 版权所有,并保留所有权利。
Powered by Discuz, Processed in 0.032301 second(s), 7 queries.
Copyright©2004-2024, BCCN.NET, All Rights Reserved