Object-based programming techniques#
Introduction: Container-like types#
The word „Container-like” is not a Fortran term, but used in the context
of this article to designate types with components whose size (or type,
to be discussed later) is not known when the type is declared. For
deferred sizing of array objects, this can be achieved by using either
the pointer
or the allocatable
attribute for the component’s
specification.
The language features and programming techniques will be shown using two
examples introduced in the following section. The demonstration codes
for this chapter can be found in the object_based
folder of the
Github repository.
Examples for definitions of container-like types#
Allocatable components#
As an example for the type definition of a value container (not a
Fortran term) with an allocatable
component consider
type :: polynomial
private
real, allocatable :: a(:)
end type
An object declared to be of this type
type(polynomial) :: p
is suitable for characterization of a polynomial
\(p(x) = \sum_{k=0}^{\text{degree}} a_{k} \cdot x^k \quad (x \in \Re)\)
once it has been created and subsequently supplied with values of the coefficients:
degree = ... ! integer value known at run time only
allocate( p%a(0:degree) )
p%a(0:) = ...
Pointer components#
As an example for the type definition of a reference container (not
a Fortran term) with a pointer
component consider
type :: sorted_list
private
type(sortable) :: data
type(sorted_list), pointer :: next => null()
end type
Note that referencing the type itself when declaring a component is
permitted if that component has the pointer
or allocatable
attribute; such types are generally known as recursive. They are
used to represent information structures (lists, trees, …), often with
specific relationships between the individual data entries stored in
each node. In this example, the assumption is that entries of type
data
in subsequent list items fulfill an ordering condition, based on
the functionality supplied with that type:
type, public :: sortable
character(len=:), allocatable :: string
end type
interface operator(<) ! compare two objects of type sortable
module procedure less_than ! implementation not shown here
end interface
Wskazówka
Given that Fortran supports arrays, use of simple linked lists is in most cases inappropriate. The example is presented here as being the simplest that permits illustrating the language features of interest.
An object declared to be
type(sorted_list) :: my_list
is suitable as starting point for building a linked list with node
entries of type data
. In the simplest case, inserting a data item into
the object is done by executing the following statements:
type(sortable) :: my_data
:
my_data = ...
my_list%data = my_data ! only compiles if type definition is accessible in host
However, as we shall see below, setting up a complete and valid
sorted_list
object in a reliable manner needs additional work.
Constructing objects of container-like type#
The semantics of the default structure constructor for container-like
objects needs to account for any additional pointer
or allocatable
attribute specified for type components.
For the first example type from the last section, the executable statements in
type(polynomial) :: q, r
:
q = polynomial( [2., 3., 1.] )
r = polynomial( null() )
result in an object q
auto-allocated to the value
q%a(1:3) == [2., 3., 1.]
, and an object r
with r%a
unallocated.
For the second example type from the last section, the executable statements in
type(sorted_list) :: sl1
type(sorted_list), target :: sl2
type(sortable) :: d1, d2
:
sl1 = sorted_list( data=d1, next=sl2 ) ! use keyword notation
sl2 = sorted_list( d2, null() )
result in an object sl1
with sl1%next
pointer associated with sl2
,
and an object sl2
with sl2%next
disassociated; the data
components
of both objects have values, d1
and d2
, respectively. Note that an
argument that matches with a pointer
component must have either the
pointer
or the target
attribute. Also, keyword notation can
be used in structure constructors in the same manner as for procedure
arguments.
The default constructor’s behaviour has some properties that one needs to be aware of:
If all type components have the
private
attribute i.e., the type is opaque (not a Fortran term), it can only be used if the type declaration is accessed by host association (this is the same as for nonallocatable/nonpointer components);especially for container-like types, its semantics may be incompatible with the programmers intentions for how the objects should be used.
Item 2 is illustrated by the above object setups, specifically:
In the
polynomial
example given above, the lower bound ofq%a
is set to 1, contrary to the expectation that it should be 0. One could account for this by calculating index offsets in any module procedures that processpolynomial
objects, but this makes the code harder to understand and maintain. Also, the degree of the polynomial should be determined by the last nonzero entry of the coefficient array, but the language can of course not be aware of this.In the
sorted_list
example given above, the ordering requirement for entries in subsequent nodes is not checked, so will usually be not fulfilled. Also, ifsl2
goes out of scope beforesl1
does, the list structure is torn to bits.
The programmer can enforce appropriate semantics by overloading the structure constructor. In this case, it is usually a good idea to declare the types as being opaque.
Overloading the structure constructor is done by
creating a named interface (i.e., a generic function) with the same name as the type of interest;
creating at least one specific function (a subroutine is not permitted), usually returning a scalar result of the type of interest.
For the polynomial
type the interface block (placed in the
specification section of the module containing the type definition)
might read
interface polynomial
! overload to assure correct lower bound when creating a polynomial object
module procedure :: create_polynomial
... ! further specifics as needed
end interface
and the implementation of create_polynomial
(in the contains
part of
the module) might read
pure type(polynomial) function create_polynomial(a)
real, intent(in) :: a(0:)
integer :: degree(1)
degree = findloc( a /= 0.0, value=.true., back=.true. ) - 1
allocate( create_polynomial%a(0:degree(1)) )
create_polynomial%a(0:) = a(0:degree(1))
end function
Because its signature matches the default structure constructor’s, the function actually overrides the default constructor, making it generally unavailable.
For the sorted_list
type the interface block might read
interface sorted_list
! the default constructor is unavailable because the type is opaque
! the specific has a different signature than the structure constructor
module procedure :: create_sorted_list
... ! further specifics as needed
end interface
with the implementation of create_sorted_list
as follows:
pure function create_sorted_list(item_array) result(head)
type(sortable), intent(in) :: item_array(:)
type(sorted_list) :: head
integer :: i
do i = 1, size(item_array)
call add_to_sorted_list(head, item_array(i))
! handles tedious details of pointer fiddling
end do
end function
The constructor has a signature that differs from that of the default
one, but the latter is unavailable outside the host scope of the type
definition anyway, due to the opacity of sorted_list
.
Copying objects of container-like type#
Default assignment extends to container-like objects. For objects declared as
type(polynomial) :: p, q
type(sorted_list) :: slp, slq
... ! code that defines p, slp
and after defining values for prospective right-hand sides, execution of the statement
q = p
produces the same result as
if ( allocated(q%a) ) deallocate( q%a )
q%a = p%a ! performs auto-allocation using the RHS's bounds, then copies the value
and execution of the statement
slq = slp
produces the same result as
slq%data = slp%data
slq%next => slp%next ! creates a reference between list objects without copying any value
The terms deep copy and shallow copy (neither are Fortran terms)
are sometimes used to describe the above behaviour for allocatable
and
pointer
components, respectively. Note that - different from the
default structure constructor - having private
components does not
affect the use of default assigment. However, the semantics of default
assignment might not be what is needed from the programmer’s point of
view.
Specifically, consider the case where the object slq
above has
previously been set up by invoking the overloaded constructor. The
assignment above would then have the following effects:
The list elements of the original
slq
, beginning withslq%next
, would become inaccessible („orphaned”), effectively causing a memory leak;after the assignment statement,
slq%next
references intoslp%next
, resulting in aliasing.
To avoid 2., it is possible to overload the assignment operator for reference containers to create a deep copy. Note that in the case where defined unary or binary operations are introduced, the functions that define these need to create deep copies to create the result variable anyway, otherwise things simply don’t work. The downside of this is that in code like
slq = slp // slq
– with the overloaded concatenation operator meaning that the argument
lists are joined – multiple deep copies need to be done (the
implementation of the module procedure join_lists
that supplies the
necessary specific for //
is not shown here; see the source
code sorted_list.f90
for details). It turns out that some of these
exist only intermediately.
Here an implementation of the specific procedure for the overloaded
assignment of sorted_list
objects:
subroutine assign_sorted_list(to, from)
type(sorted_list), intent(in), target :: from
type(sorted_list), intent(out), target :: to ! finalizer is executed on entry,
! see below for discussion of this.
type(sorted_list), pointer :: p, q
p => from; q => to
deep_copy : do
if ( associated(p) ) then
q%data = p%data
else
exit deep_copy
end if
p => p%next
if ( associated(p) ) allocate( q%next )
q => q%next
end do deep_copy
end subroutine
Avoiding 1. is usually done by means of finalizers, to be discussed in
the next section. This is because assignment is not the only possible
cause for orphaning of pointer
-related memory (or indeed other
resource leaks).
Finalization and conclusions#
To deal with resource leaks that are otherwise not within the
programmer’s means to avoid, a type definition can be connected with a
user-defined final procedure that is automatically invoked in
certain situations. For the sorted_list
type, this would look like
type :: sorted_list
private
type(sortable) :: data
type(sorted_list), pointer :: next => null()
contains
final :: delete_sorted_list
end type
Note that the final
statement appears after a contains
statement in
the type definition; this implies that delete_sorted_list
is not a
regular type component. The module procedure’s implementation might then
be as follows:
pure recursive subroutine delete_sorted_list(list)
type(sorted_list), intent(inout) :: list
if ( associated(list%next) ) then
deallocate( list%next ) ! invokes the finalizer recursively
end if
end subroutine
It must be a subroutine that takes a single argument of the type to be finalized. Most additional attributes are not permitted for that dummy argument; for the case of finalizing array arguments it is possible to have a set of finalizers (all listed in the type definition), each of which declares the dummy argument with an appropriate rank.
Wskazówka
The pure
and recursive
properties specified above reflect the
specific needs for the sorted_list
type and its associated procedures.
The recursive
specification is optional (i.e., procedures can be
called recursively by default), but a non_recursive
specification can
be supplied if the implementation’s semantics does not permit correct
behaviour in recursive calls.
The finalizer will be automatically invoked on an object if
it appears on the left-hand side of an intrinsic assignment statement (before the assignment is performed),
on invocation of a procedure call where it is argument associated with an
intent(out)
dummy,it is a non-saved variable and program execution ends its scope, or
it is deallocated.
Nonpointer nonallocatable function results fall into the third category above; however, finalization does not apply for the default structure constructor.
Note that if a finalizer is defined and the constructor is overloaded,
but the assignment operator is not, then the assignment statement
slq = sorted_list(...)
(which then translates into a single function
call to the create_sorted_list()
function shown earlier) will result
in a mutilated left-hand side, because the finalizer will be executed on
the function that overloads the constructor, resulting in slq%next
being disassociated. For this reason, the following guideline applies:
Recommendation:
Finalizers, overloads for the default constructor, and overload of the assignment operation should usually be jointly implemented.
See also the article „Rule of three” for the analogous situation in C++.
Further language features useful for object-based programming#
Extended semantics for allocatable objects#
Scalars can have the allocatable
attribute:
character(len=:), allocatable :: my_string
type(sorted_list), allocatable :: my_list
Allocation then can be done explicitly; the following examples
illustrate applications of the allocate
statement that are useful or
even necessary in this context:
allocate( character(len=13) :: my_string ) ! typed allocation
allocate( my_list, source=sorted_list(array_of_sortable) ) ! sourced allocation
Typed allocation is necessary for the string variable, because the length parameter of a string is part of its type; we will later see that derived types can also appear in the type specification. Sourced allocation permits the creation of an allocated object that is a clone of the specified source object or expression.
Alternatively, allocatable objects (be they scalar or arrays) can be auto-allocated by appearing on the left-hand side of an intrinsic assignment statement:
my_string = "anything goes" ! auto-allocated to RHS length before value is transferred
! my_list = sorted_list(array_of_sortable)
! the above statement would fail for an unallocated object, because the assignment
! has been overloaded using a nonallocatable first dummy argument
A caveat is that for overloaded assignment, this will usually not work - either one needs to explicitly allocate the object before assigning to it, or sourced allocation must be used, which bypasses the overloaded assignment.
Note that for allocatable objects with deferred-size entries (e.g., strings, arrays) a non-conformable left-hand side in an assignment statement will be deallocated before being allocated to the right length or shape, respectively.
Wskazówka
The features discussed in this subsection are also useful for object-oriented programming, with additional semantics applying for the case of polymorphic objects.
Implementing move semantics#
Sometimes it may be necessary to make use of move instead of copy semantics i.e., create a copy of an object and then getting rid of the original. The simplest way of doing this is to make use of allocatable (scalar or array) objects,
type(sorted_list), allocatable :: my_list, your_list
After your_list
has been set up, the object’s content can then be
transferred to my_list
by using the move_alloc
intrinsic,
call move_alloc(your_list, my_list)
which will deallocate my_list
if necessary, before doing the transfer.
After the invocation, my_list
will have the value formerly stored in
your_list
, and your_list
will end up in the deallocated state. Note
that the latter does not involve a regular object deallocation
(effectively, a descriptor for the object is moved), so any existing
finalizer will not be invoked.
The block
construct#
The above rules on finalization imply that variables declared in the
specification part of the main program are not finalizable, since they
by default have the save
attribute. One could argue this is not
necessary since all assigned memory is reclaimed when program execution
ends. However, excessive memory consumption or the use of other
resources may cause issues for reliable program execution. To work
around these, the block
construct can be used:
program test_sorted_list
use mod_sortable
use mod_sorted_list
implicit none
:
work : block
type(sortable) :: array(items)
type(sorted_list) :: my_list, ...
: ! initialize array
my_list = sorted_list(array)
:
end block work ! finalizer is executed on my_list, ...
:
end program
The construct (as the only one in Fortran) permits declaration of
non-saved variables in its specification part. Their lifetime ends when
program execution reaches the end block
statement, and they therefore
are finalized at this point, if applicable. Named variables declared
outside the construct are accessible inside it, unless a block-local
declaration with the same name exists.
Wskazówka
Note that the construct’s execution flow can be modified by
executing an exit
statement in its body; this can, for example, be
used for structured error handling and finally permits sending go to
to retirement.
The associate
construct#
With the introduction of deeply nested derived types, code that needs
access to ultimate components can become quite hard to read. An
associate
block construct that enables the use of auto-typed aliases
can be used. This is illustrated by a procedure that is used to
implement the multiplication of two polynomials:
pure type(polynomial) function multiply_polynomial(p1, p2)
type(polynomial), intent(in) :: p1, p2
integer :: j, l, lmax
lmax = ubound(p1%a,1) + ubound(p2%a,1)
allocate( multiply_polynomial%a(0:lmax) )
associate( a => p1%a, b => p2%a, c => multiply_polynomial%a, &
jmax => ubound(p1%a,1), kmax => ubound(p2%a,1) ) ! association list
do l = 0, lmax
c(l) = 0
do j = max(0, l-kmax), min(jmax, l)
c(l) = c(l) + a(j) * b(l-j)
end do
end do
end associate
end function
For the duration of execution of the construct, the associate names can
be used to refer to their selectors (i.e., the right-hand sides in the
association list). If the selectors are variables, so are the associate
names (a
, b
, c
in the above example), and can be assigned to. If
the selectors are expressions, so are the associate names (jmax
,
kmax
in the above example).
Associated entities that refer to variables inherit the dimension
,
codimension
, target
, asynchronous
and volatile
attributes from
their selectors, but no others. An associate name can only refer to an
optional
dummy argument if the latter is present. Associate names can
also appear in other block constructs (select type
, change team
),
which will be discussed where appropriate.
Performing I/O with objects of container-like type#
For objects of container-like type, a data transfer statement
type(sorted_list) :: my_list
: ! set up my_list
write(*, *) my_list
would fail to compile, since the run-time library is incapable of dealing with the irregular structures that are hiding behind the innocuous variable. Language features for user-defined derived type I/O (UDDTIO) permit the programmer to control the data transfer in an appropriate manner. This is achieved by binding an I/O statement on a derived-type object to a user-defined procedure, for example through a suitably written named interface:
interface write(formatted)
module procedure write_fmt_list
end interface
Note that this also applies to data types for which the above stand-alone statement is permitted, and then overloads the default I/O mechanism.
Once the binding is properly defined, the above I/O statement is accepted by the compiler, and its execution causes the user-defined procedure to be invoked. Therefore it is called the parent I/O statement. The actual data transfer statements that are issued inside the user-defined procedure are called child I/O statements.
The following interface variants are permitted, with the obvious interpretation:
write(formatted)
read(formatted)
write(unformatted)
read(unformatted)
The self-defined procedure is restricted with respect to its interfaces» characteristics, which are described in the following:
subroutine <formatted_io> (dtv, unit, iotype, v_list, iostat, iomsg)
subroutine <unformatted_io> (dtv, unit, iostat, iomsg)
The placeholders <formatted_io>
and <unformatted_io>
must be replaced by
a specific procedure name referenced in the generic interface.
The dummy arguments» declarations and meaning are:
dtv
: Must be declared to be a nonpointer nonallocatable scalar of the type in question. If the type is extensible (to be explained later), the declaration must be polymorphic (i.e. usingclass
), otherwise non-polymorphic (usingtype
). Itsintent
must bein
forwrite(...)
, and „out
” or „inout
” forread(...)
. It represents the object on which data transfer statements are to be executed.Wskazówka
Note: For the examples in this chapter, we need to use
class
, but the behaviour is as iftype
were used, as long as the actual arguments are non-polymorphic and the procedure-based interface is used for the invocation.unit
: Aninteger
scalar withintent(in)
. Its value is that of the unit used for data transfer statements. Use of other unit values is not permitted (except, perhaps,error_unit
for debugging purposes).iotype
: Acharacter(len=*)
string withintent(in)
. This can only appear in procedures for formatted I/O. The following table describes how the incoming value relates to the parent I/O transfer statement:
Value |
Caused by parent I/O statement |
---|---|
|
|
|
|
|
|
v_list
: A rank-1 assumed-shapeinteger
array withintent(in)
. This can only appear in procedures for formatted I/O. The incoming value is taken from the final part of theDT
edit descriptor; in the example from the table above it would have the value[10,2]
. Free use can be made of the value for the disposition (formatting, controlling) of I/O transfer statements inside the procedure. The array’s size may be zero; specifically, it will be of size zero for the listdirected or namelist cases.iostat
: Aninteger
scalar withintent(out)
. It must be given a value consistent with those produced by non-UDTTIO statements in case of an error. Successful execution of the I/O must result in a zero value. Unsuccessful execution must result in either a positive value, or one of the valuesiostat_end
oriostat_eor
from theiso_fortran_env
intrinsic module.iomsg
: Acharacter(len=*)
string withintent(inout)
. It must be given a value if a non-zeroiostat
is returned.
Additional properties and restrictions for UDDTIO are:
All data transfers are executed in non-advancing mode. Any
advance=
specifier will be ignored;asynchronous I/O is not supported;
Inside the user-defined routine, no file positioning statements are permitted.
The following demonstrates a partial implementation of formatted writing
on sorted_list
objects:
recursive subroutine write_fmt_list(dtv, unit, iotype, v_list, iostat, iomsg)
class(sorted_list), intent(in) :: dtv
integer, intent(in) :: unit, v_list(:)
character(len=*), intent(in) :: iotype
integer, intent(out) :: iostat
character(len=*), intent(inout) :: iomsg
character(len=2) :: next_component
if ( associated(dtv%next) ) then
write(next_component, fmt='("t,")')
else
write(next_component, fmt='("f")')
end if
select case (iotype)
case ('listdirected')
write(unit, fmt=*, delim='quote', iostat=iostat, iomsg=iomsg) &
dtv%data%string
case ('namelist')
write(unit, fmt=*, iostat=iostat, iomsg=iomsg) '"', &
dtv%data%string, '",', trim(next_component)
case default
iostat = 129
iomsg = 'iotype ' // trim(iotype) // ' not implemented'
return
end select
if ( associated(dtv%next) ) then
call write_fmt_list(dtv%next, unit, iotype, v_list, iostat, iomsg)
end if
end subroutine
Uwagi:
The namelist itself is inaccessible from the procedure; it is not needed since the procedure only needs to write the list values in a suitably formatted way. Termination of the list is indicated by a final logical value of
F
in the list entry of the namelist file; the termination information must be appropriately processed in the corresponding namelist case of the read procedure.The example implementation does not support
DT
editing; invoking the parent I/O statement from the above table would therefore cause error termination unless aniostat=
argument is added to it.