Fortran的FINAL子程序实际使用时是否足够可靠?
Fortran FINAL析构器:编译器行为差异与标准合规性分析
我最近深入测试了现代Fortran中用FINAL关键字实现的析构器特性,发现不同编译器之间的行为差异大到让人怀疑这个特性的实际可用性。下面是我的测试分析和相关疑问:
一、FINAL析构器的基础实现
先给大家看一个标准的析构器实现示例:
MODULE mobject TYPE :: tobject ! Data declarations CONTAINS FINAL :: finalize END TYPE CONTAINS SUBROUTINE finalize(object) TYPE(tobject) object ... END SUBROUTINE END MODULE
这个模块定义了一个带析构器的类型,析构逻辑写在finalize子程序里。
二、核心问题:编译器行为的显著分歧
我测试了Intel Fortran 19、GFortran 7.4.0和8.2.1.2,结果发现两款编译器的行为完全不一致:
- GFortran:不会销毁数组中存储的对象;
- Intel Fortran:赋值时会执行不必要的析构,甚至对含有垃圾数据的内存调用析构器,从函数返回时还会额外多调用一次析构器。
GFortran的两个版本表现完全一致,看来这是它的稳定行为。
三、分场景的详细行为对比
我把测试分成了几个典型场景,逐个对比两款编译器的表现:
1. 主程序块中的实例
- GFortran:主程序块里声明的实例,退出时不会调用析构器;
- Intel Fortran:会正常调用析构器(对应测试用例中的
run1)。
2. 标量对象
- 共性:只要标量实例完成了任意形式的初始化,两款编译器都会调用析构器;
- Intel Fortran的特殊坑:赋值函数返回值时,会先对栈上未初始化的对象调用析构器,再覆盖数据;而且
newObject函数结束时也会调用一次析构器。这就要求程序员必须显式检查实例是否初始化,不然析构器可能会处理垃圾数据。
3. 数组中的对象
- GFortran:数组超出作用域时,完全不会调用任何元素的析构器;
- Intel Fortran:是否调用析构器只看数组元素的初始化方式,和数组是不是可分配类型没关系。
4. 通过赋值初始化的可分配数组
用赋值隐含分配的现代特性时,行为和普通数组类似,但Intel Fortran不会对未初始化的实例调用析构器,这一点倒是比标量场景靠谱。
5. 函数返回的可分配对象/指针
- GFortran:函数结束时不会调用析构器,而是把对象交给客户端,等客户端释放时再调用,这符合我们的预期;
- Intel Fortran:返回可分配对象时,函数退出时会对本地值调用析构器;如果用隐含分配或者指针显式分配后赋值,还会对未初始化内存调用析构器。这个问题可以用
allocate(var); call var%init(...)的模式来规避。
四、关于Fortran标准的疑问
这些不一致性让我产生了几个关键疑问:
- 到底哪种编译器的行为完全符合Fortran标准?
- Fortran标准对于析构器的触发条件是不是存在模糊性?
- 标准里有没有什么条款会导致这种反直觉的行为?
五、测试代码与输出
下面是我用的测试代码和运行结果,大家可以自己复现测试:
Makefile(用于编译运行不同版本编译器)
!! -- Makefile --------------------------------------------------- !! Runs the code with various compilers. SHELL = bash FC = NO_COMPILER_SPECIFIED COMPILERS = gfortran-7 gfortran-8 ifort PR = @echo$(n)pr -m -t -w 100 define n endef all: rm -rf *.mod *.bin $(foreach FC, $(COMPILERS), $(n)\ rm -rf *.mod && \ $(FC) destructor.f90 -o $(FC).bin && \ chmod +x $(FC).bin) $(PR) $(foreach FC, $(COMPILERS), <(head -1 <($(FC) --version))) $(info) $(foreach N,0 1 2 3 4 5 6,$(n) \ $(PR) $(foreach FC, $(COMPILERS), <(./$(FC).bin $(N))))
Fortran测试代码
!! -- destructor.f90 --------------------------------------------- module mobject implicit none private public tobject, newObject type :: tobject character(32) :: name = "<undef>" contains procedure :: init final :: finalize end type tobject contains subroutine init(object, name) class(tobject), intent(inout) :: object character(*), intent(in) :: name print *, "+ ", name object%name = name end subroutine init function newObject(name) type(tobject) :: newObject character(*), intent(in) :: name call new%init(name) end function newObject subroutine finalize(object) type(tobject) :: object print *, "- ", object%name end subroutine finalize end module mobject module mrun use mobject implicit none contains subroutine run1() type(tobject) :: o1_uninit, o2_field_assigned, o3_tobject, o4_new, o6_init type(tobject), allocatable :: o5_new_alloc, o7_init_alloc print *, ">>>>>> run1" o2_field_assigned%name = "o2_field_assigned" o3_tobject = tobject("o3_tobject") o4_new = newObject("o4_new") o5_new_alloc = newObject("o5_new_alloc") call o6_init%init("o6_init") allocate(o7_init_alloc) call o7_init_alloc%init("o7_init_alloc") print *, "<<<<<< run1" end subroutine run1 subroutine run2Array() type(tobject) :: objects(4) print *, ">>>>>> run2Array" objects(1)%name = "objects(1)_uninit" objects(2) = tobject("objects(2)_tobject") objects(3) = newObject("objects(3)_new") call objects(4)%init("objects(4)_init") print *, "<<<<<< run2Array" end subroutine run2Array subroutine run3AllocArr() type(tobject), allocatable :: objects(:) print *, ">>>>>> run3AllocArr" allocate(objects(4)) objects(1)%name = "objects(1)_uninit" objects(2) = tobject("objects(2)_tobject") objects(3) = newObject("objects(3)_new") call objects(4)%init("objects(4)_init") print *, "<<<<<< run3AllocArr" end subroutine run3AllocArr subroutine run4AllocArrAssgn() type(tobject), allocatable :: objects(:) print *, ">>>>>> run4AllocArrAssgn" objects = [ & tobject("objects(1)_tobject"), & newObject("objects(2)_new") ] print *, "<<<<<< run4AllocArrAssgn" end subroutine run4AllocArrAssgn subroutine run5MoveAlloc() type(tobject), allocatable :: o_alloc print *, ">>>>>> run5MoveAlloc" o_alloc = getAlloc() print *, "<<<<<< run5MoveAlloc" end subroutine run5MoveAlloc function getAlloc() result(object) type(tobject), allocatable :: object print *, ">>>>>> getAlloc" allocate(object) object = newObject("o_alloc") print *, "<<<<<< getAlloc" end function getAlloc subroutine run6MovePtr() type(tobject), pointer :: o_pointer print *, ">>>>>> run6MovePtr" o_pointer => getPtr() deallocate(o_pointer) print *, "<<<<<< run6MovePtr" end subroutine run6MovePtr function getPtr() result(object) type(tobject), pointer :: object print *, ">>>>>> getPtr" allocate(object) object = newObject("o_pointer") print *, "<<<<<< getPtr" end function getPtr end module mrun program main use mobject use mrun implicit none type(tobject) :: object character(1) :: argument print *, ">>>>>> main" call get_command_argument(1, argument) select case (argument) case("1") call run1() case("2") call run2Array() case("3") call run3AllocArr() case("4") call run4AllocArrAssgn() case("5") call run5MoveAlloc() case("6") call run6MovePtr() case("0") print *, "####################"; print *, ">>>>>> runDirectlyInMain" object = newObject("object_in_main") print *, "<<<<<< runDirectlyInMain" case default print *, "Incorrect commandline argument" end select print *, "<<<<<< main" end program main
测试输出
>> make rm -rf *.mod *.bin rm -rf *.mod && gfortran-7 destructor.f90 -o gfortran-7.bin && chmod +x gfortran-7.bin rm -rf *.mod && gfortran-8 destructor.f90 -o gfortran-8.bin && chmod +x gfortran-8.bin rm -rf *.mod && ifort destructor.f90 -o ifort.bin && chmod +x ifort.bin pr -m -t -w 100 <(head -1 <(gfortran-7 --version)) <(head -1 <(gfortran-8 --version)) <(head -1 <(ifort --version)) GNU Fortran (SUSE Linux) 7.4.0 GNU Fortran (SUSE Linux) 8.2.1 2 ifort (IFORT) 19.0.4.243 2019041 pr -m -t -w 100 <(./gfortran-7.bin 0) <(./gfortran-8.bin 0) <(./ifort.bin 0) >>>>>> main >>>>>> main >>>>>> main #################### #################### #################### >>>>>> runDirectlyInMain >>>>>> runDirectlyInMain >>>>>> runDirectlyInMain + object_in_main + object_in_main + object_in_main <<<<<< runDirectlyInMain <<<<<< runDirectlyInMain - <undef> <<<<<< main <<<<<< main - object_in_main <<<<<< runDirectlyInMain <<<<<< main pr -m -t -w 100 <(./gfortran-7.bin 1) <(./gfortran-8.bin 1) <(./ifort.bin 1) >>>>>> main >>>>>> main >>>>>> main >>>>>> run1 >>>>>> run1 >>>>>> run1 + o4_new + o4_new - <undef> + o5_new_alloc + o5_new_alloc + o4_new + o6_init + o6_init - <undef> + o7_init_alloc + o7_init_alloc - o4_new <<<<<< run1 <<<<<< run1 + o5_new_alloc - o7_init_alloc - o7_init_alloc - o5_new_alloc - o6_init - o6_init + o6_init - o5_new_alloc - o5_new_alloc + o7_init_alloc - o4_new - o4_new <<<<<< run1 - o3_tobject - o3_tobject - <undef> - o2_field_assigned - o2_field_assigned - o2_field_assigned <<<<<< main <<<<<< main - o3_tobject - o4_new - o6_init - o5_new_alloc - o7_init_alloc <<<<<< main pr -m -t -w 100 <(./gfortran-7.bin 2) <(./gfortran-8.bin 2) <(./ifort.bin 2) >>>>>> main >>>>>> main >>>>>> main >>>>>> run2Array >>>>>> run2Array >>>>>> run2Array + objects(3)_new + objects(3)_new - <undef> + objects(4)_init + objects(4)_init + objects(3)_new <<<<<< run2Array <<<<<< run2Array - <undef> <<<<<< main <<<<<< main - objects(3)_new + objects(4)_init <<<<<< run2Array <<<<<< main pr -m -t -w 100 <(./gfortran-7.bin 3) <(./gfortran-8.bin 3) <(./ifort.bin 3) >>>>>> main >>>>>> main >>>>>> main >>>>>> run3AllocArr >>>>>> run3AllocArr >>>>>> run3AllocArr + objects(3)_new + objects(3)_new - <undef> + objects(4)_init + objects(4)_init + objects(3)_new <<<<<< run3AllocArr <<<<<< run3AllocArr - <undef> <<<<<< main <<<<<< main - objects(3)_new + objects(4)_init <<<<<< run3AllocArr <<<<<< main pr -m -t -w 100 <(./gfortran-7.bin 4) <(./gfortran-8.bin 4) <(./ifort.bin 4) >>>>>> main >>>>>> main >>>>>> main >>>>>> run4AllocArrAssgn >>>>>> run4AllocArrAssgn >>>>>> run4AllocArrAssgn + objects(2)_new + objects(2)_new + objects(2)_new <<<<<< run4AllocArrAssgn <<<<<< run4AllocArrAssgn - objects(2)_new <<<<<< main <<<<<< main <<<<<< run4AllocArrAssgn <<<<<< main pr -m -t -w 100 <(./gfortran-7.bin 5) <(./gfortran-8.bin 5) <(./ifort.bin 5) >>>>>> main >>>>>> main >>>>>> main >>>>>> run5MoveAlloc >>>>>> run5MoveAlloc >>>>>> run5MoveAlloc >>>>>> getAlloc >>>>>> getAlloc >>>>>> getAlloc + o_alloc + o_alloc + o_alloc <<<<<< getAlloc <<<<<< getAlloc - `4�\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0 <<<<<< run5MoveAlloc <<<<<< run5MoveAlloc - o_alloc - o_alloc - o_alloc <<<<<< getAlloc <<<<<< main <<<<<< main - o_alloc <<<<<< run5MoveAlloc - o_alloc <<<<<< main pr -m -t -w 100 <(./gfortran-7.bin 6) <(./gfortran-8.bin 6) <(./ifort.bin 6) >>>>>> main >>>>>> main >>>>>> main >>>>>> run6MovePtr >>>>>> run6MovePtr >>>>>> run6MovePtr >>>>>> getPtr >>>>>> getPtr >>>>>> getPtr + o_pointer + o_pointer + o_pointer <<<<<< getPtr <<<<<< getPtr - `��\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0 - o_pointer - o_pointer - o_pointer <<<<<< run6MovePtr <<<<<< run6MovePtr <<<<<< getPtr <<<<<< main <<<<<< main - o_pointer <<<<<< run6MovePtr <<<<<< main
内容的提问来源于stack exchange,提问作者kdb
相关产品推荐
相关产品推荐

