毕苏萍,周振红,赫晓慧
(1.郑州大学 土木工程学院,河南 郑州450001;2.郑州大学水利与环境学院,河南郑州450001)
在计算机科学与技术领域,泛型编程(Generic Programming)具有广泛的意义.用泛型编程先驱(Alexander Stepanov)的话来说:泛型编程是对算法、数据结构进行抽象和分类,其目标是递增式构造实用、高效、抽象的算法、数据结构的系统目录结构或框架[1].简言之,泛型编程是将算法、数据结构由具体的实例提升到一般、抽象的形式,使之可以操作不同的数据类型.
C++提供了模板(包括类模板和函数模板),并逐步积累有相对完善的标准模板库STL[2],对泛型编程给予了很好的支持.Fortran从77到90[3]、2003[4]对泛型编程的支持不断加强,但直至2008也没能提供模板工具[5].假如 Fortran 90借用C++函数模板能够获得成果,那么无疑会极大地拓展C++的应用空间,给科学与工程计算增添新的活力.笔者就此展开探讨,示例程序测试环境:C++为 VC 6.0,Fortran 90为 Compaq Visual Fortran 6.6.
C++支持函数重载,允许在参数表不同的前提下于同一编译单元定义几个同名函数,调用时依据参数表最佳匹配的原则自动选择合适的函数.比如在编程计算中,1/2整数除结果为0,1.0/2.0实数除结果为0.5,笔者用两个重载函数予以验证(当中的参数采取引用传递,和Fortran 90的参数传递保持一致):
int divid(int&a,int&b){return a/b;}
float divid(float&a,float&b){return a/b;}.
测试上列重载函数的主函数为:
void main(void){
int a=1,b=2;float x=1.0,y=2.0;
cout<< ″1/2=″<< divid(a,b) << endl;//整数除
cout<<″1.0/2.0=″<< divid(x,y) <<endl;}//实数除.程序运行结果为:
1/2=0
1.0/2.0=0.5.
观察上列重载函数,不难发现两个特点:①是接口类同,惟有函数结果、参数的数据类型不同;②是算法相同.在这种情况下,将上列重载函数抽象为一个函数模板、用一个泛型T代替函数结果、参数的数据类型:
template<typename T>//亦可用class代替typename声明函数模板中的泛型
T divid(T&a,T&b){return a/b;}//divid,a,b的类型均为泛型T
同样的测试主函数,当调用divid(a,b)函数时构造的是divid<int>函数模板实例,而当调用divid(1.0,2.0)函数时则构造的是divid<float>函数模板实例,分别与整型和实型重载函数divid相当,所以测试结果与上列重载函数的相同.说明上列重载函数与函数模板的效果完全相同,从而证明,可以将函数模板看成是一特殊的重载函数簇.
要模拟C++函数重载,有必要先回顾一下Fortran 90接口块的引入.Fortran 90共有4种程序单元:主程序、外部例程(子程序和函数统称为例程)、模块和数据块,当被调程序为外部例程时,为使编译器产生正确调用,Fortran 90要求在调用程序中建立被调外部例程的接口块,以明确其接口信息:例程名、例程实现机制(函数,或者子程序)、函数类型、参数的类型、属性及传递方式.当被调外部例程接口简单时,是否在调用程序中建立其接口块是可选的;当接口复杂时,建立其接口块就成为必须的.比如:外部函数返回数组或变长字符串,参数中有可选参数,有假定形状数组、指针或目标属性参数,有例程参数(即例程作参数,类似于C语言中的函数指针作参数)等.接口块的构造形式为:
Interface
Function/Subroutine例程名 (形参表)!接口
形参声明(包括函数结果类型声明)
End Function/Subroutine
End Interface.
Fortran 90不直接支持例程重载,不允许定义同名的外部例程,但允许将几个外部例程接口置于同一接口块内,并给接口块命名、以接口块名作为各个外部例程的统称,调用时依据接口匹配的原则自动选择相对应的外部例程,从而推出了支持泛型编程的接口块(姑且称为泛型接口块).
Interface泛型接口块名
接口体
End Interface.
其中,接口体由几个外部例程或者模块例程接口构成.
下面用Fortran 90实现前述C++函数重载示例.首先,用外部例程(函数)div_int和div_real分别实现C++整数除和实数除重载函数,其实现代码只比各自的接口多一行.
div_int=x/y或div_real=x/y
包含其泛型接口块(divid)的主程序为:
PROGRAM test_overloading
Implicit None
Interface divid!泛型接口块
Integer Function div_int(x,y)!外部例程接口
Integer,Intent(IN)::x,y
End Function
Real Function div_real(x,y)!外部例程接口
Real,Intent(IN)::x,y
End Function
End Interface
WRITE(* ,*)'1/2=',divid(1,2)!整数除
WRITE(* ,*)'1.0/2.0=',divid(1.0,2.0)!实数除
END PROGRAM.程序运行结果为:
1/2=0
1.0/2.0=0.500 000 0.
调用程序使用了统一的泛型接口块名divid,而真正调用的是与接口匹配的div_int、div_real外部例程或称为“重载”例程;C++尽管重载函数名称相同,但由于编译时增加的特殊修饰其目标函数名并不相同,这样才有可能依据不同的参数表调用与之匹配的重载函数.可见:这里的外部例程加泛型接口块与C++重载函数的效果是相同的.
无论是C++的重载函数还是C++的函数模板,都只有在C++环境中才能直接调用或实例化,即便在其子集C语言中也无法直接使用.推想背后的道理,可能是编译器的功能所致.C++编译器能够添加特殊的命名修饰,据此可以判明对应的重载函数或构造不同的函数模板实例;C编译器无此功能,所以它不支持函数重载或函数模板,C++的重载函数或函数模板也禁止使用C链接(其作用是消除C++编译器的特殊命名修饰).
前面笔者已经探讨过:Fortran 90在泛型接口块的支持下,可以将普通外部例程当作是C++的重载函数,进而也可以看成是C++函数模板实例.这样一来,如果设法在C++环境中将函数模板实例化为 Fortran 90“重载”例程,就可采取C++与 Fortran的混合编译[6],从而在 Fortran 90环境中使用C++函数模板.循这一思路,在前述C++函数模板示例代码下面增加包装子
extern ″C″{
int__stdcall DIV_INT(int&a,int&b){return divid(a,b);}
float__stdcall DIV_REAL(float&a,float&b){return divid(a,b);}}
为使接口与Fortran 90的“重载”例程接口保持一致,上列设置采取C链接、__stdcall调用约定、大写命名约定及引用参数传递方式.此处的包装子有两个作用:对内,实例化函数模板;对外,承担Fortran 90“重载”例程.
将前述C++函数模板和包装子单独保存为一个文件(.cpp),并与 Fortran 90主程序文件(.f90)置于同一项目.程序运行结果,与模拟C++函数重载示例的结果相同.
将C++函数模板看成接口相似、算法相同的特殊重载函数簇,在泛型接口块支持下,将Fortran 90外部例程模拟成C++重载函数,然后在C++环境中添加包装子,将函数模板实例化成Fortran 90“重载”例程,进而在Fortran 90环境中以正常方式使用C++函数模板.像C等其它语言要借用C++函数模板,也可采取同样的思路.
[1]ALEXANDER A.STEPANOV.Generic programming[EB/OL].http://www.stepanovpapers.com/,2012.5.22.
[2]DAVID V,NICOLAI M J.C++Templates:The Complete Guide[M].Addison Wesley,2003.
[3]周振红,郭恒亮,张君静,等.Fortran 90/95高级程序设计[M].郑州:黄河水利出版社,2005.
[4]Fortran 2003 standard[EB/OL].http://www.j3-fortran.org/doc/year/04/04-007.pdf,2012.5.22.
[5]CHIVERS S.Introduction to programming with fortran with coverage of fortran 90,95,2003,2008 and 77[M].Springer,2012.
[6]任慧,周振红,张成才.Fortran与C/C++的混合编译[J].计算机工程与设计,2007,28(17):4096-4098、4111.