利用C++模板技术支持多种计算策略

武汉理工大学   孟岩
任何有经验的程序员和软件设计师都知道,在软件开发中最常见的困境的并不是在问题面
前束手无策,而是在一大堆好像都可行的解决策略中选择一个。软件设计就是选择的过程
,其困难之处也就在于你得不断地、小心翼翼地做出各种选择。在传统的程序设计模式下
,每一次选择都是不可逆转的,你做出了选择,那么随后的开发都将会受到这次选择的剧
烈影响。当你发现自己当初的选择错误的时候,可能一切都已经为时太晚。这有点像是走
迷宫,而走迷宫时如果你发觉走进了死胡同,还可以掉头换一条路线。但在软件开发中,
更换计算策略的成本太高,几乎是无法承受的。如果能够有一种软件设计方法,使更换计
算策略的成本大大降低,使你有更多的“悔棋”的机会,那么软件设计的难度就可以大大
降低,质量就会得到很大的提高。
另一个常常出现而且令人烦恼的问题是这样的,我们在软件的开发中经常会遇到同类型的
问题反复地以不同面貌出现,每次都让你在直觉上感到这几个问题好像差不多。但是当你
满怀信心的企图实现一个可重用组件,一劳永逸地解决问题的时候,你就会沮丧地发现,
一些细节上的不同使你很难、甚至根本没有办法实现这样一个可重用的组件。于是你不得
不一次次地重写类似的代码,使得程序冗长,难于维护。如果我们有一种高度抽象的软件
开发技术,把所有细节上的不同都抽象起来,包装在一个统一的接口背后,那么我们就有
能力来达成更高程度的代码可重用性,写出适用性更强的软件组件。显然,在这些细节里
,类型是一个重要方面,而另一个重要方面就是计算策略。
综合这两点,我们可以看到,把计算策略从一个软件组件中抽象出来将会非常有益。这当
然不是一件容易的事情,不光需要在设计上做大量分析,还需要一些特别的编程技术。本
文试图用C++提供的模板技术,来给出解决这个问题的一种思路。
1. 一个实际例子
我拿一个手头的例子来说明,在施工进度控制系统里,有一个这样的功能:在已知各种必
要信息的情况下,需要估算进行某项工作需要花费的时间。在这里,我的角色不是应用程
序的开发者,而是支持组件的开发者。也就是说,我来提供组件,由客户程序员具体使用
,完成特定的任务。在这样一个简单的任务里,充满了各种选择,我们把这些选择简述如
下:
基本计算模型:可以采用经验估计法,也可以用公式计算法,用户还可能有其他更贴切的
计算模型。
精确度要求:通常只有两个选项,一是采用精确计算,二是采用模糊数学理论处理。
环境干扰因素的影响(天气、气候等):可以按照最理想、最糟糕、历年平均和今年预计
四种计算策略处理,用户还可能有其他的选项。
其实还可以找出一些策略,但是用来说明问题,上面给出的足够了,单在这里就总共提供
了2*2*4 = 16种策略组合。
作为组件开发者,我不可能知道客户程序员在开发应用软件时需要用什么样的策略组合。
在形成早期计划时,需要理想估算,可以用公式计算法+精确计算+历年平均策略组合。在
施工过程中,经验来得更重要一些,需要用经验估算法+模糊计算+今年预计这个组合形式
,施工单位还可能想知道自己最好和最差的情形,也许还会用到预期自然状况为最理想和
最糟糕的计算策略。总之,写这个组件的时候我根本不知道应用开发者会怎样使用它,具
体的策略组合只有到面对具体问题时才能由应用开发者做出决定,我不能替他们提前做决
断,我的任务是必须不折不扣地提供全部的组合可能。
怎么办?将这16种不同方案逐一写来?上面的任何一个策略只要多出一个选择项,组合数
目就增加数个,更有甚者,如果突然你发现还需要再多考虑一个因素,比如工作者执行计
划的能力因素,那么组合方案会几倍增加。这太可怕了。计算机科学界有一句名言:“永
远不要对抗组合爆炸。”所以,我们必须设法重用代码。
现在我把问题归纳如下:
需要设计一种方案来实现一个叫做WorkingTimeEstimater的可重用组件,可以根据使用这
个组件的客户程序员的意志自由地组合各种计算策略,并表现出相应的行为。
2. 利用多态性
我们理想的情况是这样的,写一个WorkingTimeEstimater类,在这个类中间有一个成员函
数estimate(),用来算出当前工序的耗时,这个类大致的框架看起来像这个样子:
class WorkingTimeEstimate {
public:
     const Time estimate();  //计算当前工序耗时,返回表示时间的Time值
     …
private:
     list<WorkProc>& works_;  //WorkProc代表一个工序,works_
                                   //是准备计算的一系列工序
     list<WorkProc>::iterator current_; //当前工序
     …
};
图1
其中WorkingTimeEstimater::estimate()的伪代码是这样的:
用基本计算模型计算current_工序基准时间t,函数名CalcBasicTime;
按精确度要求修正t,FixAccuracy;
按干扰因素修正t,FixEnvirEffect;
返回t;
estimate()高度一致的算法使我们大有希望实现代码的高度重用。但是,当你深入考虑时
,就会发现这是很困难的。记住,我们必须提供各种可能的策略组合,所以绝对不能把具
体的选项硬编码在estimate()里。
一个可能的解决方法是建立如下的类关系:
 
三个抽象基类AbstractAlgo,AbstractAccuracyFixer和AbstractEnvirFixer分别代表抽象
的三种计算策略,而在WorkingTimeEstimater中针对每一个抽象类设置一个引用成员变量
如下:
class WorkingTimeEstimate {
     …
private:
     AbstractAlgo& basicAlgo_;
     AbstractAccuracyFixer& accuFixer_;
     AbstractEnvirFixer& envirFixer_;
};
const Time WorkingTimeEstimate::estimate() {
     Time t = basicAlgo_.CalcBasicTime(current_);
     t = accuFixer_.FixAccuracy(current_, t);
     t = envirFixer_.FixEnvirEffect(current_, t);
     return t;
}
图3
这样一来客户程序员可以这样构造一个WorkingTimeEstimate对象:
WorkingTimeEstimate wte(/*其他参数*/,
FormulaAlgo(), 
AccuracyFixer(),BestEnvirFixer());
这个wte对象初始化时使用了公式计算+精确修正+最佳自然条件的策略组合,如果客户程序
员需要别的策略组合,只要在初始化时提供别的策略参数组合。由于CalcBasicTime(), F
ixAccuracy(), FixEnvirEffect()都是虚函数,wte.estimate()计算中多态性发挥威力,
完成客户程序员预期的功能。
我们对这个技术方案做一些分析。首先,代码的重用是基于组合,而不是继承,耦合度低
,比较符合OOP的原则。将basicAlgo_, accuFixer_, envirFixer_设为引用型变量,可以
强制客户程序员在使用一个WorkingTimeEstimate对象时,必须在初始化时给出具体的计算
策略。此外,构造函数WorkingTimeEstimate::WorkingTimeEstimate()可以利用缺省参数
机制为客户程序员提供一个缺省的策略组合。如果客户程序员认为有必要增加新的计算策
略,他所需要做的只是按照文档说明写一个新的派生类实现自己的计算策略,生成一个对
象,然后把这个对象传给WorkingTimeEstimater的构造函数。如果客户程序员感到自己用
错了计算策略,很简单,只要更动一行代码就解决问题。
问题到这里似乎解决得很好,但是实际的软件开发中,情形往往并不如此理想,让我们来
看一下这种解决方案的几个局限性:
1) 只提供了一种缺省的策略组合。我们是要给客户程序员以充分的选择权,但是也应该化
简他们的工作,为他们提供几个最常见的组合,免得每次生成一个新的wte对象都要对着文
档看半天。由于C++的缺省参数机制不支持这一点,你只能提供一个缺省策略组合。
2) 一个不容易发现的问题:内存泄漏(memory leak)。我们如果这样调用WorkingTimeEst
imater的构造函数:
WorkingTimeEstimate wte(/*其他参数*/,
*new FormulaAlgo(), 
*new AccuracyFixer(),
*new BestEnvirFixer());
那么生成的这三个对象在wte.estimate()返回之后就会永远留在heap中的某处。这个问题
归根结底是因为我们要利用多态性,就不得不使用polymorphic class,使用虚函数。实际
上CalcBasicTime(), FixAccuracy(), FixEnvirEffect()三个成员函数在功能上都与各自
对象的this指针没什么关系,如果可能的话最好设为static member function。但是stat
ic成员函数不能同时是virtual的,也不可能具有多态性的表现。 唯一的办法是在说明文
档里强调不要使用上面的格式初始化wte,然后寄希望于你的老板雇一些比较认真的客户程
序员。
3) 假设在所有16种策略组合方案之中,有一类是与众不同的:当出现公式计算法+模糊处
理策略组合时,WorkingTimeEstimater::estimate()的算法将发生很大变化,这对我们是
很大的问题。只好冒险重载estimate(),通过传递参数的不同来选择不同的estimate()算
法。显然,这又要寄希望于高水平的客户程序员。如果他传递了不正确的参数,系统可能
会犯下神秘的错误。
4) 最后,一个真正的麻烦。在上面的WorkingTimeEstimate::estimate()的实现中,我们
很惬意的写下这样的代码:t = basicAlgo_.CalcBasicTime(current_); 并期望这行代码
能够根据basicAlgo_绑定的实际对象表现出多态性。这本身就假定StatAlgo::CalcBasicT
ime()和FormulaAlgo::CalcBasicTime()具有相同的function signature,都需要一个lis
t<WorkProc>::iterator型参数。但是,如果这个假设不成立,比如StatAlgo::CalcBasic
Algo()声明为:
Time StatAlgo::CalcBasicAlgo(list<WorkProc>::iterator iter,
                                   const StatInfo& si);
那么我们就会遇到很大的麻烦。AbstractAlgo::CalcBasicAlgo()的函数签名应该是什么?
怎么写WorkingTimeEstimate::estimate()?即使现在没有出现这个问题,如果以后要向A
bstractAlgo类层次里加入新的派生类,也难免遇到问题。我们该怎么设计一个“强壮”的
基类?这个问题恐怕困扰过每一个设计过OOP程序的人,从而得了一个专门的名字:fragi
le base class(脆弱的基类)。
上面的一些问题,是不是让你感到没有希望了呢?其实也不是没希望,仍旧在OOP领域里,
仍旧利用polymorphism机制,我们总能够想到一些办法来解决这些问题。比如,我们禁止
所有的策略类在heap中建立对象,就可以避免上述的第二个问题(怎么做?参见Meyers96
, Item 27)。但是,别忘了本文的题目是“利用C++模板技术支持多种计算策略”,所以
我们不能再与polymorphism纠缠不休了。
3. Policy方案
从现在开始,我们要进入主题了。我们要对付的问题是多策略,而这回我们的武器是C++ 
template。侯捷先生在他的STL系列文章里提出这样的观点:template与polymorphism一样
,都可以实现代码重用,只不过后者是在纵向上(类派生层次)实现,而前者是在横向上
实现。既然也能够实现重用性,那么我们就可以把上面基于多态性的多策略方案转而用模
板来实现。一旦使用template复用方案,就不再是面向对象程序设计(OOP),而是泛型程序
设计(Generic Programming)了。泛型程序设计研究方面公认的专家Andrei Alexanderesc
u在其最新著作Modern C++ Design中系统地描述了以模板实现的代码复用技术,并且规定
了一些新的概念和术语。Policy(直译为“政策”)就是一个关键的概念。一个Policy定
义了一个类介面或一个类模板介面(interface),该介面由以下语言结构中的一个或若干个
组成:内部定义的类型(包括嵌套的typedef),成员函数和成员变量。凡是提供了符合这个
介面定义的类或类模板就成为一个Policy。使用policy的类称为host class。若干个提供
相同介面的Policy可以互相替换,从而使相同的host class代码表现不同的功能行为。

我们把上面的例子简化来说明这个概念,只考虑一个基本算法策略AlgoPolicy。我们要求
所有的AlgoPolicy类(或模板类)都必须提供一个这样的静态成员函数:static Time Ca
lcBasicTime(list<WorkProc>::iterator)。然后我们编写StatAlgoPolicy如下:
class StatAlgoPolicy {
typedef list<WorkProc>::iterator LI;
    static Time GetShortestTime(const LI& currwork);
     static Time GetLongestTime(const LI& currwork);
public:
     static Time CalcBasicTime(const LI& currwork) {
         // 下面的具体算法读者不必关心
     Time ts = GetShortestTime(currwork);
          Time tl = GetLongestTime(currwork);
          double tm = (ts + tl) / 2.0;
          return (ts + tl + 4 * tm) / 6.0;
     }
};
图4
上面这个StatAlgoPolicy提供了符合AlgoPolicy介面规定的CalcBasicTime()成员函数,执
行估算法计算基本时间,因此是一个符合要求的AlgoPolicy。
有了Policy,我们规定host class如下:
template <class AlgoPolicy>
class WorkingTimeEstimater {
public:

const Time estimate() {
 Time t = AlgoPolicy::CalcBasicTime(current_);
          //设法修正t
          return t;
     }
private:
     list<WorkProc> works_;
     list<WorkProc>::iterator current_;
};
图5
然后生成一个WorkingTimeEstimater<StatAlgoPolicy>对象:
WorkingTimeEstimater<StatAlgoPolicy> wte;
然后wte.estimate()的行为就是执行估算功能。如果我们再提供一个AccuracyAlgoPolicy
,同样遵守AlgoPolicy的介面规范,则如下生成wte对象:
WorkingTimeEstimater<AccuracyAlgoPolicy> wte;
那么wte.estimate()执行的就是精确计算。如果你把不遵守AlgoPolicy介面规范的类作为
模板参数传给WorkingTimeEstimater<>,则编译器一定会报错。也就是说,跟OOP一样,编
译器会保证你必须遵守规则,无需你自己担心。
照葫芦画瓢,现在我们扩大战线,把所有的三个Policy考虑在内,得到下面的WorkingTim
eEstimater定义:
template <class AlgoPolicy,  // 基本算法Policy
class AccuracyPolicy, // 精确性Policy
class EnvirPolicy  // 环境影响Policy
           >
class WorkingTimeEstimater {
public:

const Time estimate() {
 Time t = AlgoPolicy::CalcBasicTime(current_);
          t = AccuracyPolicy::FixAccuracy(current_, t);
          t = EnvirPolicy::FixEnvirEffect(current_, t);
          return t;
     }
private:
     list<WorkProc> works_;
     list<WorkProc>::iterator current_;
};
图6
我们考虑一下这个技术方案跟前面用多态性实现的方案相比有什么优缺点。首先,他至少
与前面的方案有相同的灵活性。其次,由于不是采用虚函数后期绑定,没有运行时开销,
也没有vptr和vtable带来的空间开销,所以时空效率都比原方案要好。第三,刚才提到的
向客户程序员提供数种缺省的组合方案,在多态型方案里很困难,但是用这里的模板方案
就轻而易举:
typedef WorkingTimeEstimater<StatAlgoPolicy, FuzzyPolicy,
PredEnvirPolicy> NormalEstimater;
typedef WorkingTimeEstimater<FormulaAlgoPolicy, AccuratePolicy,
BestEnvirPolicy> IdealEstimater;
就这样短短两条语句,向客户程序员提供了两个缺省组合方案。用户只要用声明语句Idea
lEstimater iesmtr;就可以获得一个理想WorkingTimeEstimater对象iesmtr。如果想增加
更多的缺省组合方案,很简单,只要增加typedef即可。第四,不必担心有什么内存泄漏的
问题,这一点不言自明。第五,扩充新的Policy非常方便,不用继承、不用改写虚函数,
只要写一个类,符合相应的Policy介面规范,然后作为模板参数传给WorkingTimeEstimat
er,这样新的Policy就可以在WorkingTimeEstimater<>::estimate()算法中发挥作用。

4. 更进一步
我们在前面的多态性解决方案里提到特殊策略组合带来的特殊算法问题。借助template s
pecialization(模板特殊化)技术,我们可以给出比较妥善的解决办法。所以我们首先要
介绍一下模板技术的一些要点和template specialization技术。
模板提供了一个对所有类型都适用的通用方案。例如:
template <class T> void swap(T& a, T& b);
你可以在代码中以两个double或者两个std::complex类型左值(lvalue)作为参数调用swap
(),C++编译器会自动地根据上面的那个模板格式为你生成针对具体类型的swap()。由于模
板具现化(template instantiation)是一个编译时行为,而且是按需提供,也就是说,你
用到了,它才具现化,你没用到,它就会对这些模板代码视而不见(即使有错误也发现不
了)。所以这一切就好像是编译器里嵌入了一个很聪明的代码产生机(code generator),
可以根据你的模板形式和实际代码自动地为你写代码。C++强大的模板技术和精致的具现化
机制使得这个代码产生机非常地聪明和强大。利用这个机制进行程序设计,现在已经发展
成为一个专门的方向。可以毫不客气地说,这种方式提供了比OOP更高一层的抽象,因而使
程序获得更大的灵活性。
上面的swap()模板,是针对所有类型的通用函数模板,也就是说所有的类型都会采用相同
的算法处理,这并不总是一件好事。假设现在你有一种类型ST,这种类型做swap动作时有
一种特别高效的算法,那么你可以为这种类型定义一个特殊的swap()版本,与上面通用的
swap<T>()形成重载(overload)关系。
void swap(ST& a, ST& b) {…}
根据调用重载函数时的解析规则,编译器在遇到swap()的时候,会根据实际参数寻找最匹
配的swap()来调用。这就使得你可以给以ST类型的swap()操作以特殊的礼遇。函数模板借
助重载机制达成的这种效果,在类模板里如何实现呢?这就要依赖一个比较新的C++语言特
性——模板特殊化(template specializtion,其实也不是新特性,早在1995年就写入C++
标准草案第一稿,但是时至今日,还有些主流的编译器如Visual C++不支持此特性)。让我
们用实例来说明。
回到我们的时间估计组件。假设在所有16种策略组合方案之中,有一类是与众不同的:当
出现公式计算法+模糊处理策略组合时,WorkingTimeEstimater::estimate()的算法将发生
很大变化,那么在提供了图6的通用WorkingTimeEstimater定义之外,我们可以利用局部模
板特殊化(partial template specialization)定义一个特殊的WorkingTimeEstimater如下

template <class EnvirPolicy> // 模板参数表中只留下环境影响Policy
// 而在下面的<>中间显式指定两个策略
class WorkingTimeEstimater<FormulaAlgoPolicy, FuzzyFixPolicy> {
public:

const Time estimate() {
 … // 提供特殊的算法
     }
private:
     list<WorkProc> works_;
     list<WorkProc>::iterator current_;
};
图7
编译器在具现化具体的WorkingTimeEstimater类时,如果发现有FormulaAlgoPolicy和Fuz
zyFixPolicy的组合,就会自动找到最合适类模板定义,也就是图7的类模板来具现化。因
此,你提供的特殊的estimate()就会起作用。这样就可以为公式计算法+模糊处理策略组合
的情况提供特殊的estimate()实现。问题得到了解决。
现在回过头去看看在第2节中对于对态性解决方案提出的四个责难,现在已经解决了三个,
还剩下一个“脆弱基类”问题还没有解决。实际上这个问题也可以用局部模板特殊化来解
决。但是从理论上讲,如果在你使用Policy抽象隔离出可能发生变化的代码段后,estima
te()算法还可能因为别的因素而产生变体,那只能说明一点:你的抽象工作进行得不够彻
底。按照Andrei Alexandrescu的观点,在Policy-based的设计中,最困难的莫过于发掘出
所有的Policies,同时还要尽可能让这些Policies之间保持正交关系,也就是各Policy彼
此之间互相独立,互不影响。这叫做正交分解(orthogonal decomposition)。STL的一个伟
大贡献就是实现了算法和容器组件之间的正交分解:算法不知道其所作用的容器,容器也
不知道哪些算法会作用在其上,两者通过iterator来契合,反而有更大的自由度。这与OO
P中把算法与数据结构捆绑封装在一起的思想相比,可谓是别有洞天。而与“程序=数据结
构+算法”的传统思想虽然貌似一致,但是在抽象层次上已经不能同日而语了。
5. 推荐阅读
为了说明本文中描述的技术,我根据Andrei Alexandrescu所著Modern C++ Design一书中
的代码片断扩充了一个完整的例子程序creator.cpp,实现了创建对象的三种策略(用new
,用malloc,用clone),用到了一些比较高级的C++语言特性,如template template pa
rameter,并加上了比较详细的注释,读者有兴趣的话可以看看。请用Borland C++Builde
r 5.0中的bcc32.exe命令行编译工具这样编译:bcc32 creator.cpp
另外,policy在很多方面与traits技术很相似,而对于traits技术的阐释,以及template
 partial specialization技术,侯捷先生发表在《程序员》2001年第3、4期上的专栏文章
可以说是精彩之至。读者不可错过。另外在侯捷网站上有作者翻译的一篇Alexandrescu发
表在C++ Report上的traits文章,也可以作参考。
Alexandrescu的Modern C++ Design第一章在更严格的意义上和更高的层次上描述了polic
y-based design的思想和方法。有条件的读者一定要读一读这本书。(完)



--
************************************* 
|    手持智慧剑      身披大度甲     | 
|    爱心传四方      仗义走天下     | 
|                                   | 
|                      --- 小李飞刀 | 
************************************* 

※ 来源:.南京大学小百合站 http://bbs.nju.edu.cn [FROM: 211.67.20.209]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值