莫华?/p>
GPLQ也是General Purpose LanguageQ是我们使用的最多的一c语a。传l上QGPL的语法,或者特性,是固态的。然而,E序员都是聪明hQ即便算不上“最聪明”Q也得?“很聪?#8221;?)Q,往往不愿受到语法的束~,试图按自q心意“攚w?#8221;语言。实际上Q即便是早期的语aQ也提供了一些工P供聪明h们玩弄语法。我看的W一本C语言的书里,有q么一个例子,展示U?#8220;邪恶”的手D:
#define procedure void
#define begin {
#define end }
然后Q?br> procedure fun(int x)
begin
...
end
实际上,它的意思是Q?br> void fun(int x)
{
...
}
q可以看作是对初学C语言的PascalE序员的安慰。这U蹩脚的戏法可以作元编E的一U,在一U语a里创造了另一个语法。不q,实在有些无聊。然而,在实际开发中Q我们或多或地会需要一些超法范围的机制。有时ؓ了完善语aQI补一些缺憾;有时Z增强一些功能;有时Z获得一些方ѝ更新潮的,是试囑֜一UGPL里构建Domain Specific LanguageQ或者说“子语a”Q以获得某个Ҏ领域上更直观、更z的~程方式。这些对语言的扩展需求的实现Q依赖于被称为Meta- ProgrammingQMPQ的技术?br> 另一斚wQ随着语言功能和特性的不断增加Q越来越多的人开始抱怨语a太复杂。一斚wQ?#8220;N我们会需要那些一辈子也用不到几回的语a机制Q来增加语言的复杂性和学习使用者的负担吗?”。另一斚wQ?#8220;有备无患Q一个语a机制要到q在眉睫的时候才去考虑吗?”。但MP技术则这对矛盾消弭于无Ş。一U语aQ可以简z到只需最基本的一些特性。而其他特定的语言功能需求,可以通过MP加以扩展。如果不需要某U特性,那么只要不加载相应的MP代码卛_Q而无需为那些机制而烦恹{?br> MP最׃h的地方,莫过于我们可以通过~写一个代码库便得语a具备以往没有的特性?br> 然而,全面的MP能力往往带来巨大的副作用Q以至于我们无法知道到底是好处更多,q是副作用更多。语a的随意扩展往往带来某些危险Q比如语法的冲突和不兼容Q对基础语言的干扎ͼ关键字的泛滥{等。换句话_MP是孙悟空Q本领高强。但没有紧箍咒,是管不住他的?br> 那么Q紧咒是什么呢Q这是q里打算探讨的主题。本文打通过观察两种已存在的MP技术,分析它们的特点与~陷Q从中找决问题的Q可能)途径?
首先Q先来看一下宏Q这U远古时代遗留下来的技术。以及它的后裔,ast宏?br> 关于传统的宏的MP功能Q上面的代码已经单地展示了。但是,q种功能是极其有限的。宏是通过文本替换的Ş式,把语a中的一些符受操作符、关键字{等替换成另一UŞ式。而对于复杂的语法构造的创徏无能为力。问题的另一面,宏带来了很多副作用。由于宏的基是文本替换,所以几乎不受语法和语义的约束。而且Q宏的调试困难,通常也不受命名空间的U束。它带来的麻烦,往往多于带来的好处?br> ast宏作Zl宏的后l者,做了改进Q得宏可以在astQAbstract Syntax TreeQ结构上执行语法的匹配。(q里需要感?a >TopLanguage上的Olerev兄,他用z而又清晰的文字,Ҏq行了ast宏的初培训:)Q。这P便可以创造新的语法: 下面Q我们回q头Q再来看看另一UMP技术——TMPQ参考David Abrahams, Aleksey Gurtovoy所著的《C++ Template Metaprogramming》)。对于TMP存在颇多争议Q支持者认为它提供了更多的功能和灵zL;反对者认为TMPq于trickyQ难于运用和调试。不怎么PTMP的出现向我们展示了一U可能性,卛_GPL中安全地q行MP~程的可能性。由于TMP所q用的都是C++本n的语a机制Q而这些机刉是相容的。所以,TMP所构徏?MP体系不会同GPL和其他子语言的语法机制相冲突?br> 实际上,TMP依赖于C++的模板及其特化机制所构徏的编译期计算体系Q以及操作符的重载和模板化。下面的代码摘自boost::spirit的文: expr1 = integer | group; expr2 = expr1 >> *(('*' >> expr1) | ('/' >> expr1)); q里表达了一lEBNFQ语法着实古怪,q咱待会儿再_Q?gt;>代表了标准EBNF?#8220;followed by”Q?代表了标准EBNF?Q从双Ud左边Q,括号q是括号Q|依旧表示Union。通过对这些操作符的重载,赋予了它们新的语义(即EBNF的相兌义)。然后配合模板的cd推导、特化等{机Ӟ变戏法般地构造出一个语法解析器Q而且是编译时完成的? 管在spirit中,>>?、|{操作符被挪作他用,但是我们依然可以在这些代码的前后左右插入诸如Qcin>> *ptrX;的代码,而不会引发Q何问题。这是因?gt;>{操作符是按照不同的cd重蝲的,对于不同cd的对象的调用Q会调用不同版本的操作符重蝲Q互不干扎ͼ老少无欺? 但是QTMP存在两个问题。其一Q错误处理不뀂如果我不小心把W二行代码错写成Qexpr1 = i | group;Q而i是一个intcd的变量,那么~译器往往会给Z些稀奇古怪的错误。无非就是说cd不匹配之cȝQ但是很晦ӆ。这斚w也是TMP受h诟病的一个主要原因。好在C++0x中的concept可以Ҏ板作出约束,q且在调用点直接l出错误提示。随着q些技术的引入Q这斚w问题会得到~解? 其二Q受到C++语法体系的约束,MP无法自由地按我们习惯的Ş式定义语法构造。前面说q了Qspirit的EBNF语法与标准EBNF有不的差异Q这对于spirit的用造成了不ѝ同P如果试图q用TMP在C++中构造更高的DSL应用Q诸如一U用于记账的帐务处理语言Q将会遇到更大的限制。实际上TMP下的DSL也很有令h满意的? 所以说QTMP在用上的安全性来源于操作W复用(或重载)的特性。但是,操作W本w的语法Ҏ是固定的,q得依赖于操作W(泛化或非泛化Q重载的TMP不可能成为真正意义上的MP手段? 那么Q对于TMP而言Q我们感兴趣的是它的安全性和相容性。而对其不满的Q则是语法构造的灉|性。本着“dp粕Q取其精?#8221;的宗旨,我们可以对TMP做一番改q,以获得更完整的MP技术。TMP的核心自然是模板Q类模板和函?操作W模板)Q在concept的帮助下Q模板可以获得最好的安全性和相容性。以此ؓ基础Q如果我们将模板扩展到语法构造上Q那么便可以在保留TMP的安全性和相容性的情况下,获得更大的语法灵zL。也是_我们需要增加一U模李쀔?strong>语法模板
syntax(x, "<->", y, ";")
{
std::swap(x, y);
}
当遇C码:
x <-> y;
的时候,~译器用std::swap(x,y);加以替换。实际上Q这是将一U语法结构映到另一个语法结构上。而ast宏则是这U映的执行者?br> 但是Qast宏ƈ未消除宏本n的那些缺陗如果x或者y本n不符合swap的要求(cd相同Qƈ且能复制构造和赋|或者拥有swap成员函数Q,那么 ast宏调用的时候无法对此作出检验。宏通常以预~译器处理,ast宏则其推迟到语法分析之时。但是此时依然无法得到x或y的语义特征,无法直接在调用点l出错误信息?br> 同时Qast宏还是无法处理二义性的语法构造。如果一个ast宏所定义的语法构造与主语aQ或者其他ast宏的相同Q则会引发乱。但是,如果单粗暴地这U?#8220;重定?#8221;作ؓ非法处理Q那么会大大~小ast宏(以及MPQ的应用范围。实际上Q这U语法构造的重定义有其现实意义,可以看作一U语法构造的“重蝲”Q或者函敎ͼ操作W)重蝲的一U扩展?br> 解决的方法ƈ不复杂,是为ast宏加上约?/strong>。实际上Q类似的情Ş在C++98的模板上也存在,而C++则试N过为模板添加conceptU束加以解决。这U约束有两个作用Q其一Q在W一旉对ast宏的使用q行正确性检验,而无需{到代码展开之后Q其二,用以区分同一个语法构造的不同版本?br> 于是Q对于上qC子可以这h加约束(q些代码只能表达一个意思,q无法看作真正意义上的MP语法Q:
syntax(x, "<->", y, ";")
where x,y is object of concept (has_swap_mem or (CopyConstructable and Assignable))
&& typeof(x)==typeof(y)
{
std::swap(x,y);
}
如此Q除非xQy都是对象Qƈ且符合所指定的conceptQ否则编译器会当卛_以拒l,而且直截了当?br> 不过Q如此变化之后,ast宏将不会再是宏了。因U约束是语义上的Q必ȝ到语义分析阶D,方能验。这pZ宏的领地了。不q既然ast宏可以从预处理阶D|q到语法分析阶段Q那么再推迟一下也没关pR再_我们x的是q种功能Q带U束的ast宏到底是不是宏,也无关紧要?/p>
TMP
group = '(' >> expr >> ')'; expr = expr2 >> *(('+' >> expr2) | ('-' >> expr2));
template<typename T>
syntax synSwap=x "<->" y ";"
require SameType<decltype(x), T> && SameType<decltype(y), T>
&& (has_swap_mem<T> || (CopyConstructable<T> and Assignable<T>)
{
std::swap(x, y);
}
q里我杜C关键字syntaxQƈ且允ؓ语法构造命名,便于使用。而语法构造描qͼ则是{号后面的部分。require沿用自C++0x的concept提案。稍作了些改造,允许concept之间的||?
如果比较带约束的ast宏和语法模板Q会发现极其怼。实际上Q两者殊途同归,展示了几乎完全相同的东西。ast宏和TMP分别位于同一个问题的两端Q当它们的缺陷得到I补时Q便会相互靠拢,最l归l到一UŞ式上?nbsp;
现在Q通过l合两种Ҏ的特Ԍ我们便可以得C个最l的MP构造,既安全,又灵z:
syntax synSwap=x "<->" y ";";
where
(x,y is object)
&& SameType<x, y>
&& (has_swap_mem<x> || (CopyConstructable<x> and Assignable<x>))
{
std::swap(x, y);
}
我去掉了template关键字,在约?where)的作用下Qtemplate和类型参数列表都已经没有必要了。同Ӟ也允许直接将对象攑օ conceptQAssignable<x>Q这相当于:Assignable<decltype<x>>?
“is ...”是特别的U束Q用来描q那些超concept范畴的特性?#8220;...”可以是关键字“object”Q表明相应的标识W是对象Q也可以是关键字 “type”Q表明相应的标识W是cdQ或者是关键?#8220;concept”Q指定相应的标识W是concept{等。操作符的重载所涉及的参数只会是对象Q只需对其cd做出U束Q因此concept便够用。但语法构造的定义则广大的多,它不仅仅会涉及对象,更可能涉及其它类型的语法要素。实际上Q语法构造所面对的参数是“标识W?#8221;。那么一个标识符上可能具备的各种Ҏ,都应当作为约束的内容。这里大致归U_以下几点需要约束的Ҏ:
也可以认为,语法构造的U束是concept的自然g伸。concept对类型做出约束,而语法构造的U束的对象,则是标识W?
Z强化语法构造的创徏能力Q应当允许在语法构造的定义中用BNFQ或其他cM的语法描q语a?
在语法构造约束的支援下,便可以化解一些子语言同宿主语a之间的语法冲H。比如,我们创徏了一U类似spirit的EBNF子语aQ可以按照标准的EBNF形式~写语法Q构造一个语法解析器Q?
syntax repeat1=p '+'
where
p is object of type(Parser)
{...}
q样便定义了一个后~形式?操作W,但仅仅作用于cdParser的对象上。对于如下的代码Q?
letter + number
使用主语a?Q加Q,q是使用EBNF?Q重复,臛_一ơ)Q取决于letter和number的特征(q里是类型)Q?
int letter, number;
letter + number; //使用主语a?Q表Cletter加number
Parser letter, number;
letter + number; //使用EBNF?Q表CZ串letter后面跟一个number
如此Q便使得用户定义的语法不会同主语a的相同语法发生冲H?
但是Q语法构造的U束q不能完全消除所有的语法冲突。其中一U情况便是常量的使用。比如:
'x' + 'y'
它在宿主语言中,会被识别Z个字W的相加。但在BNF子语a中,则会是若q个字符'x'之后紧跟一?#8216;y'。由于没有类型、对象等代码实体的参与,~译器无法分辨用哪U语a的语法规则来处理。解决的Ҏ是得常量类型化。这U做法在C++{语a中已l运用多q。通常采用为常量加前后~Q?
'x'b_ + 'y'b_
以此代表BNF子语a的常量。常量的语法定义可以是:
syntax bnf_const="'" letter "'" "b_";
where
is const of ... //...可以是某U类?
{...}
通过is const ofU束Q将所定义的常量与某个cdl定。而通过cdQ编译器便可以推断出应当使用那种语法对其处理?
然而,管带约束的语法构造定义可以在很大E度上消除语法冲H和歧义Q但不可能消除所有的语法矛盾。另一斚wQ结构相|但语义完全不同的语法构造合在一P即便不引发矛盾,也会对用者造成难以预计的乱。因此,在实际环境下Q需要通过某种“语法围栏”严格限定某种用户定义语法的作用范围。最直接的Ş式,是通过namespace实现。namespace在隔d协调命名冲突中v到很好的作用Q可以进一步将其运用到MP中。由于namespace一l打开Q其作用范围不会出最内层的代码块作用域:
{
using namespace XXX;
...
} //XXX的作用范围不会超个范?
q用q个Ҏ,可以很自然地蕴藏在一个namespace中的用户定义语法构造限制在一个确定的范围内?
但是Q仅此不够。毕竟语法不同于命名Q不仅会存在冲突Q还会存在(合法的)hQ而且往往是非常危险的。ؓ此,需要允许在using namespace的时候进一步指定语法的排他性。比如:
{
using namespace XXX exclusive;
...
}
如果namespace中的用户定义语法与外部语法(宿主语言Q或外围引用的namespaceQ重叠(冲突或淆)Q外部语法将被屏蔽。更q一步,可以允许不同U别的排他:排斥重叠和完全屏蔽。前者只屏蔽重叠的语法,q种U别通常用于扩展性的用户语法构造(扩展主语aQ需要同主语a协同工作。而且语法重叠较少Q;而后者则外部语法统l屏蔽,只保留所打开的namespace中的语法Q子语言在代码块中是唯一语言Q,q种U别用于相对独立的功能性子语言Q可以独立工作的子语aQ通常会与主语a和其他子语言间存在较大的语法重叠Q比如上qBNF子语a{等Q?
为提供更多的灉|性,可以在完全屏蔽的情况下,通过特定语句引入某种不会引发冲突的外部语法。比如:
{
using namespace XXX exclusive full;
using host_lang::syntax(...); //引入主语a的某个语法,...代表语法名称
...
}
通常情况下,子语a不会完全独立q作Q需要同宿主语言或其他子语言交互Q也是数据交换。主语言代码可能需要将数据对象传递给子语aQ处理完成后再取回。由于编译器依赖于标识符的特性(主要是类型)来选择语法构造。所以,一U子语言必须使用其内部定义的cd和对象。ؓ了能够交换数据,必须能够把主语言的对象包装成子语a的内部类型。通过在子语言的namespace中进行true typedefQ可以将主语acd定义成一个新的(真)cdQ在q入子语a作用域的时候,做显式的cd转换。另外,Z方便数据转换Q还可以采用两种ҎQ将主语acdmapC个子语言cdQ相当于l予“双重国籍”Q,q入子语a的语法范围时Q相应的对象在类型上h两重性,可以被自动识别成所需的类型,但必d保不发生语法淆的情况下用;另一U稳妥些的方法,可以允许执行一UtypedefQ介于alias和true typedef之间Q它定义了一个新cdQ但可以隐式地同原来的类型相互{换。数据交换的问题较复杂,很多问题未理清Q有待进一步考察?
作ؓMP手段Qast宏拥有灵zL,而TMP则具备安全性,两者各自的优点相结合,使我们可以获得更加灵zd安全的语法构造定义手Dc通过在用户定义的语法构造上施加全面的约束,可以很好地规避语法冲H和歧义?
但是Q需要说明的是,q里所考察的仅仅局限在用户定义语法构造的冲突和歧义的消除上。GPL/MP要真正达到实用阶D,q需要面Ҏ多问题。比如,׃存在用户定义的语法构造,语法分析阶段所面对的语法不是固态的Q需要随旉地接受新语法Q甚至重叠的语法Q存在多个候选语法,不同的候选语法又会生不同的下语法Q,q就使语法分析大大复杂;语法模式匚w被推q到语义分析阶段Q此前将无法Ҏ些语法错误作出检验;一个语法构造的语义需要通过宿主语言定义Q如何衔接定义代码和周边的环境和状态;如何为用户定义的语法构造设|出错信息;׃某些语法构造的二义性,如何判别语法错误属于哪个语法构造;...。此外还有一些更本质性的问题Q诸如语法构造的重蝲和二义性是否会更容易诱使用者生更多的错误{等Q牵涉到错综复杂的问题,需要更多的分析和试验?
另外Q作为MP的重要组成部分,~译期计能力也臛_重要。TMPq用了C++模板特化QD语言通过更易于理解的static_if{机Ӟ都试图获得编译期的计能力,q些机制在完整的MP中需要进一步扩展,而ƈ非仅仅局限在与类型相关的计算上。其他一些与此相关的Ҏ,包括反射Q编译期和运行期Q、类型traits{,也应作ؓMP必不可少的特性?
然而,不久前,在TopLanguage group上的一ơ讨论,促我们注意到runtime GPq个概念。从中,我们看到了希望——GP runtime化的希望——得GP有望在运行时发挥其巨大的威力Q进一步ؓ软g的设计与开发带来更高的效率和更灉|的结构?br> 在这个新的系列文章中Q我试图q用runtime GP实现一些简单,但典型的案例Q来runtime GP的能力和限制Q同时也可以q一步探讨和展示q种技术的Ҏ?
concept表(concept listQ不再同对象攑֜一P而是同一个类型的cd信息攑֜一赗一同放|的q有ctable。ctable中每个对应的conceptw有一个指?concept表的指针Q也可以指向cd信息_Q用以找到concept listQ执行concept cast。动态对象,或动态对象的引用/指针上只需附加一个指向相应的concept的指针即可。相比前一个方案,内存利用率更高?/p>
by 莫华?
公元?16q??日,意大利东部^原,一个叫做坎的地方Q两支大军摆开阵势Q准备决一L。一Ҏ׃|斯和瓦|两位执政官率领的罗马hQ另一方则是伟大的军事天才汉尼?巴卡率领的迦太基军队及其同盟。罗马h过8万,而迦太基仅有4万余人。然而到了傍晚,|马d击|Q?万h被杀Q仅有少数得以逃脱。这是著名的坎g战。经此一役,Q外加先前进行的特利比亚和特拉西梅诺湖会战)Q罗马h元气大伤Q成q公民损p五分之一。部分城邦背叛罗马,西西里也发生起义。罗马已l到了摇摇欲坠的地步?/em>
汉尼拔的q些胜利Q完全得益于先前的一ơ异乎寻常的q征。公元前218q_汉尼拔率领军队,从新q太基城Q西班牙Q出发,越比利牛斯山,q入南高卢地域。在他面前有两条路可赎ͼ越阿尔俾斯山,或者沿岸q入意大利。但是,当时|马人已在沿地区部|了两支部队Q准备拦截汉拔。而且Q罗马h的v军优势,使得他们可以在Q何时候将一支部队登陆在他的背后。而翻阿俾斯山Q则是一条及其艰险的道\Q更何况是在冬天?/em>
汉尼拔选择了阿俾斯山。他甩开了罗马hQ从圣贝纳德和日内瓦山之间过阿尔俾斯山,q入意大利境内。此Ӟ|马Z失去了战略纵深,一把尖刀已经深深地插入他们的腹内...
C++的发展史上,也有着如同汉尼拔翻阿俾斯山q征。一切还得从C with Class时代说v?
Bjarne曄反复Q他创徏C++为的是将Simular的抽象能力同C的性能l合h。于是,在C语言的基上,诞生了一U拥有类、ѝ重载等{面向对象机制的语言。在q个阶段QC++提供了两个方面的抽象能力。一U是数据抽象Q也是数据所要表辄含义通过cd以及依附于类型上的成员表q。另一U则是多态,一U最原始的多态(重蝲Q?
数据抽象Q通过被称?#8220;抽象数据cdQADTQ?#8221;的技术实现。ADT的一U方案,是cR类所提供的封装性将一个数据实体的外在特征Q或者说语义的表qŞ式,同具体的实现Q比如数据存储Ş式,分离。这h增加的中间层数据的使用者同数据的实现者隔,使得他们使用共同的约定语义工作,不再怺了解彼此的细节,从而得两者得以解耦?
多态则是更加基更加重要的一U特性。多态得我们得以用同一U符号实现某U确定的语义。多态的_N在于Q以一UŞ式表达一U语义。在此之前,我们往往被迫使用不同的符h代表同一U抽象语义,为的是适应强类型系l所施加的约束。比如:
//代码#1
int add_int(int lhs, int rhs);
float add_float(float lhs, float rhs);
很显Ӟq两个函数表辄语义分别?#8220;把两个intcd值加在一?#8221;?#8220;把两个floatcd值加在一?#8221;。这两个语义抽象h都表达了一个意思:加?
我们在做术题的时候是不会被计算的数字是整数q是实数。同P如果能够在编E的时候,不考虑术操作对象的类型,只需兛_谁和谁进行什么操作,那么会方便得多。当C++引入重蝲后,q种愿望便得以实玎ͼ
//代码#2
int add(int lhs, int rhs);
float add(float lhs, float rhs);
重蝲使得我们只需兛_“?#8221;q个语义Q至于什么类型和什么类型相加,则由~译器根据操作数的类型自动解析?
从某U意义上_重蝲是被长期忽视Q但却极为重要的一个语aҎ。在多数介绍OOP的书c中Q重载往往被作为OOP的附属品Q放在一些不L的地斏V它的多态本质也被动多态的人造光环所N蔽。然而,重蝲的重要作用却在实践中潜移默化C现出来。重载差不多可以看作语言q入C抽象体系的第一步。它的实际效用甚臌过被广为关注的OOPQ而不会像OOP那样在获得抽象的同时Q伴随着不小的副作用?
随着虚函数的引入QC++开始具备了颇具争议的动多态技术。虚函数是一U依附于c(OOP的类型基Q的多态技术。其技术基是后期绑定(late-bindingQ。当一个类Dl承自类BӞ它有两种Ҏ覆盖QoverrideQB上的某个函数Q?
//代码#3
class B
{
public:
void fun1();
virtual void fun2();
};class D:public B
{
public:
void fun1();
void fun2();
};
当承类D中声明了同基cB中成员函数相同函数名、相同签名的成员函数Q那么基cȝ成员函数被覆盖。对于基cȝ非虚成员函数Q承类会直接将光蔽。对于类型D的用者,fun1代表了D中所赋予的语义。而类型D的实例,可以隐式地{换成cdB的引用bQ此时调用b的fun1Q则执行的是cB的fun1 定义Q而非cD的fun1Q尽此时b实际指向一个D的实例?
但是Q如果承类覆盖了基cȝ虚函敎ͼ那么得到相反的l果Q当调用引用b的fun2Q实际上却是调用了D的fun2定义。这表明Q覆盖一个虚函数Q将会在l承cd基类之间的所有层ơ上执行覆盖。这U彻底的、全方位的覆盖行为,使得我们可以在承类上修饰或扩展基类的功能或行ؓ。这便是OOP扩展机制的基。而这U技术被UCؓ动多态,意思是基类引用所表达的语义ƈ非取决于基类本nQ而是来源于它所指向的实际对象,因此它是“多?#8221;的。因Z个引用所指向的对象可以在q行时变换,所以它?#8220;?#8221;的?
随着动多态而来的一?#8220;副?#8221;Q却事实上成ZOOP的核心和支柱。虚函数?#8220;动多?#8221;Ҏ将我们引向一个极端的情况Q一个都是虚函数的类。更重要的,q个cM的虚函数都没有实玎ͼ每个虚函数都未曾指向一个实实在在的函数体。当Ӟq样的类是无法直接用的。有的是,q种被称?#8220;抽象基类”的类Q?strong>q我们l承它,q?#8220;替它”实现那些没有实现的虚函数。这P对于一个抽象基cȝ引用Q多态地拥有了承类的行为。而反q来Q抽象基cd际上起到?strong>l承cd现某些特定功能的作用。因此,抽象基类扮演?strong>接口的角艌Ӏ接口具有两重作用:一、约束承类Q实现者)q其实现预定的成员函数Q功能和行ؓQ;二、描qCl承cd定拥有的成员函数Q功能和行ؓQ。这两种作用促接口成ؓ了OOP设计体系的支柱?
C++在这斚w的进步,使其成ؓ一个真正意义上具备C抽象能力的语a。然而,q种q步q“越阿尔俾斯?#8221;。充光也只能算?#8220;越比利牛斯?#8221;。对于C++而言Q真正艰苦的q征才刚开始,那o人生畏的“阿尔俾斯?#8221;仍在遥远的前斏V?
同汉拔一P当C++一脚迈?#8220;C抽象语言׃?#8221;后,侉K临两U选择。或者在原有基础上修修补补,成ؓ一UOOP语言Q或者l前q,越那险峻的山峰。C++的汉拔——Bjarne Stroustrup——选择了后者?
从D&E的描qC我们可以看到Q在C++的原始设计中已l考虑“cd参数”的问题。但直到90q代初,才真正意义上地实C模板。然而,模板只是W一步。诸如Ada{语a中都有类似的机制Q泛型,genericQ,但ƈ未对当时的编E技术生根本性的影响?
关键性的成果来源于Alex Stepanov的A献。Stepanov在后来被UCؓstl的算?容器库上所做的努力Q得一U新兴的~程技术——泛型编E(Generic ProgrammingQGPQ——进入了Z的视野。stl的生对C++的模板机制生了极其重要的媄响,促了模板特化的诞生。模板特化表面上是模板的辅助Ҏ,但是实际上它却是?#8220;cd参数”更加本质的机能?
假设我们有一l函数执行比较两个对象大的操作Q?
//代码#4
int compare(int lhs, int rhs);
int compare(float lhs, float rhs);
int compare(string lhs, string rhs);
重蝲使得我们可以仅用compare一个函数名执行不同cd的比较操作。但是这些函数具有一L实现代码。模板的引入Q得我们可以消除这U重复代码:
//代码#5
template<typename T> int compare(T lhs, T rhs) {
if(lhs==rhs)
return 0;
if(lhs>rhs)
return 1;
if(lhs<rhs)
return -1;
}
q样一个模板可以应用于McdQ不但用一个符可达了一个语义,而且用一个实C替了诸多重复代码。这便是GP的基本作用?
接下来的变化Q可以算作真正意义上?#8220;d”了?
如果有两个指针,分别指向两个相同cd的对象。此时如果我们采用上qcompare函数模板Q那么将无法得到所需的结果。因为此时比较的是两个指针的|而不是所指向的对象本w。ؓ了应付这U特D情况,我们需要对compare?#8220;特别处理”Q?
//代码#6
template<typename T> int compare(T* lhs, T* rhs) {
if(*lhs==*rhs)
return 0;
if(*lhs>*rhs)
return 1;
if(*lhs<*rhs)
return -1;
}
q个“Ҏ版本”的compareQ对于Q何类型的指针作出响应。如果调用时的实参是一个指针,那么q个“指针?#8221;的compare会得到优先匚w。如果我们将compareҎ下面的实玎ͼ那么׃出现非常有趣的行为:
//代码#7
template<typename T>
struct comp_impl
{
int operator()(T lhs, T rhs) {
if(lhs==rhs)
return 0;
if(lhs>rhs)
return 1;
if(lhs<rhs)
return -1;
}
};
template<typename T>
struct comp_impl<T*>
{
int operator()(T* lhs, T* rhs) {
comp_impl<T>()(*lhs, *rhs);
}
};
template<typename T> int compare(T* lhs, T* rhs) {
comp_impl<T>()(*lhs, *rhs);
}
当我们将指针的指针作为实参,调用compareӞ奇的事情发生了Q?
//代码#8
double **x, **y;
compare(x, y);
compare居然成功地剥M两个指针Qƈ且正地比较了两个对象的倹{这个戏法充分利用了cL板的局部特化和特化解析规则。根据规则,是特化的模板,是优先匚w。T*版的comp_impl比T版的更加“特化”Q会得到优先匚w。那么当一个指针的指针实例化comp_implQ则会匹配T*版的 comp_implQ因为指针的指针Q也是指针。T*版通过局部特化机Ӟ剥离掉一U指针,然后用所得的cd实例化comp_impl。指针的指针剥离掉一U指针,那么q是一个指针,又会匚wT*版。T*版又会剥L一U指针,剩下的就是真正可以比较的cd——double。此Ӟdouble已无法与 T*版本匚wQ只能匹配基模板Q执行真正的比较操作?
q种奇妙的手法是蕴含在模板特化中一些更加本质的机制的结果。这U意外获得的“模板衍生产品”可以作一U编译时计算的能力,后来被一?#8220;好事?#8221;发展成独立的“模板元编E?#8221;QTemplate Meta ProgrammingQTMPQ?
管TMP新奇而又奥妙Q但l究只是一U辅助技术,用来弥补C++的一些缺陗做一些扩展,“捡个?#8221;什么的。不q它为我们带来了两点重要的启C:一、我们有可能通过语言本n的一些机Ӟq行元编E;二、元~程在一定程度上可以同通用语言一起用。这些启C对~程语言的发展有很好的指导意义?
模板及特化规则是C++ GP的核心所在。这些语aҎ的强大能力q凭空而来。实际上有一?#8220;q后大手”在冥冥之中操U늝一切?
假设有一个类型系l,包含n个类型:t1,...,tnQ那么这些类型构成了一个集合T={t1,...,tn}。在当我们运用重载技术时Q实际上构造了一l类型的tuple到函数实现的映射Q?lt;ti1,ti2,ti3,...> ->fj()。编译器在重载解析的时候,是按照q组映射L匚w的函数版本。当我们~写了Ş如代?5的模板,那么q当于构徏了映:<T, T,...> ->f0()?
而代?6Q以及代?7中的T*版模板,实际上是构造了一?lt;Tp>->fp()的映。这里Tp是T的一个子集:Tp={t'|t'=ti*, ti∈T}。换句话_特化使泛型体pȝ化了。利用模板特化技术,我们已经能够Q笨拙地Q分辨QҎ、整数、内|类型、内|数l、类、枚丄{类型。具备ؓcd划分的能力,也就是构造不同的cd子集的能力?
现在Q我们便可以构造一?#8220;泛型体系”QG={T} U T U Tp U Ta U Ti U Tf U Tc ...。其中,Tp是所有指针类型,Ta是数l,Ti是整敎ͼTf是QҎQTc是类{等。但是如果我们按照泛化程度,把G中的元素排列开Q{T, Tp, Ta, Ti,...,t1,...,tn}。我们会发现q中间存在一?#8220;断层”。这些断层位于T和Tp{之_以及Tp{与ti{之间等{。这表明在C++98/03中,抽象体系不够完整Q存在缺陗?
所以,到目前ؓ止,C++q没有真正翻阿俾斯山里那座最险峻的山峰。这正是C++0x正在努力做的Q而且胜利在望?
在C++0x中,大牛们引入了first-class的concept支持。concept目前q没有正式的法定描述Q以及合理的中文译Q。通俗地讲Qconcept描述了一个类型的Q接口)特征。说具体的,一个concept描述了类型必d备的公共成员函数Q必d备的施加在该cd上的自由函数Q和操作W)Q以及必d备的其他特征Q通过间接手段Q。下面是一个典型的conceptQ?
concept has_equal<T>
{
bool T::equal(T const& v);
};
q个concept告诉我们它所描述的类型必L一个equal成员Q以另一个同cd的对象ؓ参数。当我们这个concept施加在一个函数模板上Qƈ作ؓ对类型参数的U束Q那么就表明了这个模板对cd参数的要求:
template<has_equal T>bool is_equal(T& lhs, T const& rhs) {
return lhs.equal(rhs);
}
如果实参对象的类型没有equal成员Q那么is_equal会拒绝~译通过Q这不是我要的!
concept是可以组合的Q正式的术语叫做“refine”。我们可以通过refineq一步构造出U束更强的conceptQ?
concept my_concept<T> : has_equal<T>, DefaultConstructable<T>, Swappable<T> {}
refine获得的concept会“l承”那些“基concept”的所有约束。作为更l致的组合手D,conceptq可以通过!操作W?#8220;L”某些内涵的conceptU束Q?
concept my_concept1<T> : has_equal<T>, !DefaultConstructable<T> {}
q个concept要求cd具备equal成员Q但不能有默认构造函数?
通过q些手段Qconcept可以“无限l分”cd集合T。理ZQ我们可以制造一q串只相差一个函数或者只相差一个参数的concept?
一个concept实际上就是构成类型集合T的划分的U束QTx={ti| Cx(ti)==true, ti∈T}。其中Cx是concept所构造的U束。不同的concept有着不同范围的约束。这P理论上我们可以运用concept枚D出类型集?T的所有子集。而这些子集则正好填补了上qG中的那些断层。换句话_conceptl化了类型划分的_度Q或者说泛型的粒度。?#8220;L”的泛型系l变?#8220;q箋”的?
当我们运用conceptU束一个函数模板的cd参数Ӟ相当于用concept所描述的类型子集构Z个映:<Tx1,Tx2,...>->fx()。凡是符合tuple <Tx1,Tx2,...>的类型组合,对应fx()。所以,从这个角度而言Q函数模板的特化Q包括conceptQ可以看作函数重载的一U扩展。在concept的促q下Q我们便可以把函数模板特化和函数重蝲l一在一个体pM处理Q用共同的规则解析?
在目前阶D,C++差不多已l登上了“抽象阿尔俾斯?#8221;的顶峰。但是就如同汉尼拔进入意大利后,q需要面对强盛的|马共和国,与之作战那样。C++的面前还需要进一步将优势化作胜利。要做的事还很多Q其中最重要的,当属构徏Runtime GP。目前C++的GP是编译时机制。对于运行时x的Q务,q需要求助于OOP的动多态。但是C++领域的大牛们已经着手在Runtime GP和Runtime Concept{方面展开努力。这斚w的最新成果可以看q里?a >q里?a >q里。相信经q若q年的努力后QGP会完全的成熟,成ؓ真正L的编E技术?
坎尼会战之后Q汉拔已经拥有了绝对的优势。罗马h已经战|Q他同罗马城之间已经没有M强大的敌对力量,|马Z已经闻风丧胆Q几无斗志。但是,汉尼拔却犯下了或o他一生后悔的错误。他放过了罗马城Q{而攻ȝ马的南部城邦和同盟。他低估了罗马h的意志,以及|马同盟的牢固程度。罗马h很快l束了最初的混ؕQQ命了新的执政官,采用了坚壁清野、以柔克刚的新战略。随着旉的推U,汉尼拔和他的军队限于孤立无援的境圎ͼ被迫Z生存而作战。尽迫降ƈ占领了几个罗马城市,但是l究无法再次获得l予|马命一ȝZ?/em>
汉尼拔的战略错误实际上在从新q太基城出发之前已经注定。因为汉拔对罗马h的远征的Ҏ目的qLq占领罗马,而是通过打击|马Q削׃们的势力Q瓦解他们的联盟。以辑ֈL{订和^协议的目的。然而这U有限战略却使导致了他的最l失败?/em>
不幸的是QC++或多或少地有着同汉拔一L战略错误。C++最初的目的基本上仅仅局限于“更好的C”。ƈ且全面兼容C。这在当时似乎很合理Q因为C可以作最成功?#8220;底层高语言”Q拥有很高的性能和灵zL。但是,C的设计ƈ未考虑来会有一个叫?#8220;C++”的语a来对其进行扩展。结果很多优点对于C而言是优点,但却成了C++的负担。比如,C大量使用操作W表达语法结构,对于C而言昑־非常z,但对于C++Q则使其被迫大规模复用操作符Qؓ其后出现的很多语法缺陷埋下了伏笔。这一点上QAda做得相对成熟些。它从Pascal那里l承了主要语法,但不考虑兼容。这使得Ada更加完整Q易于发展。新语言是新语aQ过分的兼容是镣铐,不是优势。而且Q合理地l承语法Q同样可以吸引众多开发者。从l验来看Q程序员对于语法变化的承受能力还是很强的。他们更多地兛_语言的功能和易用性?/p>
另一斚wQC++最初ƈ未把目标定在“创徏一U高度抽象,又确保性能的语a”。纵观C++的发展,各种抽象机制q在完整的规划或\U图的指g加入语言。所有高U特性都是以“La战术”零打敲地加入语a。从某种E度上来看,C++更像是一U实验性语aQ而非工业语言。C++的强大功能和优点是长期积累获得的Q而它的诸多缺陷也是长期添加特性的l果?/p>
汉尼拔和C++l予我们一个很好的教训。对于一个试囑֜1?0q后依然健康成长的语aQ那么就必须在最初便拥有明确的目标和技术发展规划。对于以往的语aҎ应当择优而取Q不能照单全收。ƈ且在技术上拥有_的前L。我们知道,技术前L是很难做到的,毕竟技术发展太快。如果做不到Q那得有够的力对过ȝ东西加以取舍。所?#8220;舍小大Q弃子争?#8221;?/p>
M而言QC++在抽象机制的发展斚wQ还是成功的。尽伴随着不少技术缺P但是C++的抽象能力在各种语言中可U得上出cL萃。而且C++q在发展Q它未来会发展成什么Ş态,不得而知。但是,无论C++是l修修补补,q是Ҏ性地变革Q它的抽象能力都会不折不扣地保留Qƈ且不断完善和增强?/p>
坎尼会战之后Q汉拔又打q几ơ小规模的胜仗。但l过长期的作战,也得不到q太基的支援Q汉拔的力量越来越弱,只能在意大利半岛上勉强生存。罗马很快恢复了元气Q改革了军事体系和作战方式,重新掌握了战略主动权。更重要的是Q罗马也有了自己?#8220;汉尼?#8221;——(征服非洲的)普布利乌?#183;U尔内利乌斯·西庇阿(大西庇阿Q。西庇阿被派往北非大陆Q直接攻击迦太基人的老l。汉拔被召回,在扎马与西庇阿摆开阵势Q展开一场决战。最l,西庇阿运用从汉尼拔那里学到的战术L了迦太基人,为罗马h赢得了第二次布匿战争的胜利?/em>
此后Q汉拔在罗马h的通缉之下Q流亡于Ch岸,试图L东山再v的机会。但最l未能如愿,被迫于公元前183q自,享年64岁。有的是,他的老对手,他12岁的西庇阿也于同q去世。一个伟大的传奇此l束?/em>
class IShape
{
virtual void load(xml init)=0;
virtual void draw(monitor m)=0;
...
};
然后定义各种囑Şc,q从q个接口上承:
class Rectangle: public IShape
{
void load(xml init) {...}
void draw(monitor m) {...}
...
};
class Ellipse: public IShape
{
void load(xml init) {...}
void draw(monitor m) {...}
...
};
...
void DrawShapes(monitor m, vector<IShape*> const& g)
{
vector<IShape*>::const_iterator b(g.begin()), e(g.end());
for(; b!=e; ++b)
{
(*b)->draw(m);
}
}
...
现在可以使用q些囑ŞcMQ?/p>
vector<IShape*> vg;
vg.push_back(new Rectangle);
vg.push_back(new Ellipse);
...
DrawShapes(crt, vg);
通过接口IShapeQ我们可以把不同的图形类l一CU类型下。但是,通过虚函数的overrideQ由囑Şcd现IShape上的虚函数。这可以? 生常谈了。动多态的核心是利用override和late bound的组合,使得一个基cd以在cd归一化的情况下,拥有l承cȝ语义。OOP设计模式大量q用q种技术,实现很多需要灵zL展的pȝ?/p>
class Rectangle
{
void load(xml init) {...}
void draw(monitor dev) {...}
...
};
class Ellipse
{
void load(xml init) {...}
void draw(monitor dev) {...}
...
};
...
void DrawShapes(monitor dev, vector<anything> const& g)
{
vector<IShape>::const_iterator b(g.begin()), e(g.end());
for(; b!=e; ++b)
{
(*b).draw(dev);
}
}
...
vector<anything> vg;
vg.push_back(Rectangle(...));
vg.push_back(Ellipse(...));
...
DrawShapes(crt, vg);
class Rectangle
{
void load(xml init) {...}
void draw(monitor dev) {...}
...
};
class Ellipse
{
void load(xml init) {...}
void draw(monitor dev) {...}
...
};
...
concept Shape<T> {
void T::load(xml init);
void T::draw(monitor dev);
}
...
void DrawShapes(monitor dev, vector<Shape> const& g)
{
vector<IShape>::const_iterator b(g.begin()), e(g.end());
for(; b!=e; ++b)
{
(*b).draw(dev);
}
}
...
vector<Shape> vg;
vg.push_back(Rectangle(...));
vg.push_back(Ellipse(...));
vg.push_back(string("xxx")); //错误Q不W合Shape concept
...
DrawShapes(crt, vg);
乍看h没什么特别的Q但是请注意vector<Shape>。这?span style="font-weight: bold;">使用一个conceptQ而不是一个具体的cdQ实例化一个模?/span>。这里的意思是_q个容器接受的是所有符合Shape concept的对象,cd不同也没关系。当pushqvg的对象不W合ShapeQ便会发生编译错误?/p>
但是Q最关键的东西不在这里。注意到DrawShapes函数了吗Q由于vector<Shape>中的元素cd可能完全不同。语? (*b).draw(dev);的语义在静态语a中是非法的,因ؓ我们Ҏ无法在编译时具体定(*b)的类型,从而链接正的draw成员。而在q里Q? ׃我们引入了Runtime UnboundQ对于对象的讉K链接发生在运行时。因此,我们便可以把不同cd的对象存攑֜一个容器中?/p>
concept在这里vCcd验的作用Q不W合相应concept的对象是无法攑օq个容器的,从而在此后对对象的使用的时候,也不会发生类型失配的 问题。这也就在动态的机制下确保了cd安全。动多态确保类型安全依靠静态类型。也是所有类型都从一个抽象接口上l承Q从而将cd归一化,以获得徏立在? 态类型系l之上的cd安全。而concept的类型安全保证来源于对类型特征的描述Q是一U非侵入的接口约束,灉|性大大高于类型归一化的动多态?br>
如果我们引入q样一个规则:如果用类型创建实例(对象Q,那么所创徏的对象是静态链接的Q也是~译旉接;而用concept创徏一个对象,那么所创徏的对象是动态链接的Q也是q行旉接?/p>
在这条规则的作用下,下面q段单的代码会产生非常奇妙的效果:
class nShape
{
public:
nShape(Shape g, int n) : m_graph(g), m_n(n) {}
void setShape(Shape g) {
m_graph=g;
}
private:
Shape m_graph;
int m_n;
};
在规则的作用下,m_graph是一?strong>动态对?/strong>Q它的类型只有在q行时才能明。但是无Z么类型,必须满Shape concept。而m_n的类型是定的,所以是一?strong>静态对?/strong>?/p>
q和传统的模板有区别吗?模板也可以用不同的类型参数定义成员数据。请看如下代码:
Rectangle r;
Ellipse e;
nShape(r, 10);
nShape.setShape(e); //对于传统模板而言Q这个操作是非法的,因ؓe和r不是同一U类?/p>
动态对象的特点在于Q我们可以在对象创徏后,用一个不同类型的动态对象代替原来的Q只需要这些对象符合相应的concept。这在静态的模板上是做不到的?br>
现在回过头来看一下用concept实例化模板的情Ş。我们知道,用一个类型实例化一个模板,得到的是一个类Q或者说cd。而用一个concept实例? 一个模板,得到的又是什么呢Q还是一个concept。那么vector<Shape>是一个conceptQ因而它的实例是动态的对象。当 Ӟ实际上没有必要把vector<Shape>的实例整个地当成动态对象,它只是具有动态对象的行ؓ特征。在实现上,vector< Shape>可以按照普通模板展开Q而其内部由concept模板实参定义的对象作为动态对象处理即可?strong>一个由concept实例化的模板的对象作义上的动态对?/strong>?br> 下面的代码则引出了另一个重要的Ҏ:语言层面的concept无法做到q些Q因为它是编译期机制。ؓ此,我们需要有一U运行时的conceptQ或者说二进制别的concept?/p>
一个concept包含了与一个或若干个类型有关的一l函敎ͼ包括成员函数和自由函数。于是,我们可以用一个类?#8220;虚表”的函数指针表Q暂且称? ctable吧)存放concept指定的函数指针。这Lctable依附在动态对象上Q就像vtable一栗每个对象都会匹配和map到若q个 concept。因此,每个动态对象会有一个concept表,其中存放着指向各ctable的指针,以及相应的concept基本信息?/p>
当一?#8220;用户”Q函数或模板Q需要在q行旉接到对象上的时候,它会提交一个concept的代码(全局唯一Q。系l用q个代码在动态对象的 concept表上索,获得指向所需concept的指针,q且填写?#8220;用户”l出的一?#8220;插入?#8221;Q一个指针)中。随?#8220;用户”便可以直接通过q个 “插入?#8221;间接调用所需的函敎ͼ成员或自由函数?/p>
在这里,concept的y妙之处在于,一族函数集合在一P作ؓ一个整体(x口)。那么,在执行运行时匚w的时候,不再是一个函C个函数地查询Q? 可以一ơ性地Lq些函数是否存在。这很Ҏ地规避了cd安全保证操作的损耗。如果用hash查询Q那么可以在O(1)实现concept匚w。另 外,一个concept的hash值可以在~译时计好Q运行时链接只需执行hash表检索,qhashD也可以省去?/p>
一个动态对象可以直接用指向concept ctable的指针表C。在不同concept之间转换Q相当于改变指针的指向,q种操作非常cMOOP中的dynamic_cast?/p>
对于如下的动态对象定义:
Shape g=Cycle();
会创Z个Cycle对象Q在对象上构v一个concept表,表中对应Cycle所有符合的concept。ƈ且徏立一lctableQ每? ctable对应一个concept。每个concept表项指向相应的ctable。而符号g则实际上是指向所建立对象的Shapes ctable的指针?/p>
对于函数Q?/p>
void draw(Shape g);
draw(g);
调用gӞ׃draw的参数是Shape conceptQ而g正是draw所需的conceptQ所以无需在对象g的concept表上匚wQ可以直接用这个ctable指针。这是_只要 所用动态对象(gQ的concept同用方Qdraw函数Q能够匹配,便可以直接用指向ctable的指针链接(~译旉接)Q无需在运行时重新? 配。只有发生concept转换Ӟ才需要在concept表中搜烦Q获得所需的ctable指针Q?/p>
Swappable s=g; //Swappable是另一个concept
q种情况同dynamic_cast极其怼。也可以模仿着采用concept_cast之类的操作符Q得concept转换昑ּ化,消除隐式转换的问题(强concept化)?br>
所以,Runtime Unbound在运行时concept的作用下Q具有同动多态相同的底层行ؓ。因而,他们的性能也是一L。很多用于动多态的Ҏ和算法都可以直接用于q行时concept?br>