Modern Fortran contains various object-oriented ideas, including a concepts of "destructors" through the FINAL
keyword.
MODULE mobject
TYPE :: tobject
! Data declarations
CONTAINS
FINAL :: finalize
END TYPE
CONTAINS
SUBROUTINE finalize(object)
TYPE(tobject) object
...
END SUBROUTINE
END MODULE
However, is this feature reliable? Notably, I noticed inconsistencies about when, and whether at all, it will be called, with major differences between Intel Fortran 19 and GFortan 7, 8:
- GFortran fails to destroy objects stored inside arrays.
- Intel Fortran:
- performs spurious and potentially superfluos destructions upon assignment, potentially even on memory containing junk data, and
- performs a spurious call to the destructor upon returning from a function.
I noticed no difference between gfortran-7.4.0 and gfortran-8.2.1.2.
These inconsistencies raise some questions about the practical usability of destructors for me. Is either of the behaviors fully conforming with the standard? Is the standard unclear on this? Does the standard maybe contain clauses, that lead to unintuitive behavior?
Detailed analysis (code see below)
PROGRAM Block. Gfortran will not call the destructor for instances declared in the PROGRAM block, while Ifort will (see
run1
in example).Scalar objects. For instances declared as scalars, both Gfortran and IFort will call the destructor, if the variable has seen any form of initialization. Intel Fortran however, when assigning a function return value, will call it also
- on the uninitialzied object on the stack before overwriting it with the data from the function, and
- seemingly at the end of the
newObject
function.
This can however be guarded against by explicitly checking whether the object is initialized, before performing any cleanup.
This means, that the programmer has to explicitly check, if the instance has been initialized.
Objects in arrays. If the object is contain in an array, and the array goes out of scope,
- Gfortran will not invoke the destructor.
- Intel Fortran may invoke the destructor, depending on how a given array member was initialized.
- It makes no difference, whether the array is declared
allocatable
.
Allocatable array initialized by assignment. When using the modern feature, where assignment to an allocatable array implies allocation, the same holds, except that there are no uninitialzied instances upon which IntelFortran can call the destructor.
Allocatable/Pointers from functions.
- GFortran doesn't call the destructor at the end of the function returning the an
allocatable
object or apointer
to an object, and instead calls it when the value is deallocated in the client code, explicitly or by going out of scope forallocatable
s. That's what I expected. - Intel Fortran calls in some additional cases:
- When the object is declared
allocatable
, but not when it is apointer
, Intel Fortran invokes the destructor on the local value of the function upon exiting the function. - When initializing the object inside the function with implied allocation (
var = newObject(...)
), or in the case of thepointer
variant, with explicit allocation (allocate(var); var = newObject(...)
), the destructor is invoked on uninitialized memory, visible inrun5MoveAlloc
andrun6MovePtr
from%name
containing junk data. This can be resolved by using theallocate(var); call var%init(...)
pattern instead.
- When the object is declared
- GFortran doesn't call the destructor at the end of the function returning the an
Testing Code
!! -- 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))))
!! -- 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
Output of the testing code
>> 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