This lesson is in the early stages of development (Alpha version)

Derived types

Overview

Teaching: 10 min
Exercises: 20 min
Questions
  • How are data structures (derived types) defined in Fortran?

  • How can we control access to components of data structures in Fortran?

  • How does assignment between derived types work?

Objectives
  • Be able to declare your own data structures

  • Understand assignment operations in derived types and when shallow/deep copying occurs

Derived types provide the fundamental basis for data structures (cf. struct in C++). Derived types are sometimes referred to as user-defined types. The advice given in the introductory course was always to place type definitions in a module.

Definition

We recall that the general form of the declaration is:

 type [ [, attribute-list] :: ] type-name
    [private]
    component-part
  [ contains
    procedure-part ]
  end type [ type-name ]

Components of types may be intrinsic, allocatable, pointer, or other derived types. The procedure part is used (optionally) to define so-called type-bound procedures. These are the equivalent of class methods in other languages (and will be covered in detail in later Sections). Scope of components may be controlled via public or private statements.

Encapsulation may be achieved by declaring a public type with private components, e.g.,

type, public :: my_opaque_t
  private
  integer :: idata
end type my_opaque_t

It is also possible to have a mixture, e.g.:

type, public :: my_semi_opaque_t
  private
  integer         :: idata   ! default private
  integer, public :: ndata   ! explicitly public
end type my_semi_opaque_t

Components are referenced using the component selector %. Types with components which are themselves derived types give rise to the idea of an ultimate component, which is an intrinsic type for which no further application of % is relevant.

protected attribute

Note that there is a protected attribute in Fortran, although it has a slightly different usage than in C++. It will be discussed later as part of the section on submodules. Some authors argue that protected should be avoided as it represents a breakdown of encapsulation.

Assignments and copying

Intrinsic assignments

Intrinsic assignment is available for types, and just involves an intrinsic assignment of each component in turn. Schemetically:

  type (my_semi_opaque_t) :: a
  type (my_semi_opaque_t) :: b

  b = my_semi_opaque(2, 3)
  a = b

Here, the components of a will take on the values of the components of b.

If a type component is itself a derived type, then intrinsic assignment takes place in the same way for that component.

Example (5 minutes)

Intrinsic assignment

The accompanying module my_semi_opaque_type.f90 provides a public definition of the type as defined above, and includes a function to initialise the two components. The accompanying program exercise1.f90 will make the intrinsic assignment. You can compile with, e.g.,

$ ftn my_semi_opaque_type.f90 example1.f90

How are you going to check that the result of the assignment is correct for both components (by printing the result to the screen)? Write some code to perform this check.

Solution

subroutine my_semi_opaque_print(name, val)

  character(len=*), intent(in) :: name
  type(my_semi_opaque_t), intent(in) :: val

  print *, name, "%idata=", val%idata
  print *, name, "%ndata=", val%ndata
  
end subroutine my_semi_opaque_print

This code could be added to either the example program or the module, from a code organisation perspective the module probably makes the most sense. You should obtain the following output

gfortran my_semi_opaque_type.f90 example1.f90 && ./a.out
 a%idata=           2
 a%ndata=           3

Allocatable components

Suppose our derived type had an allocatable component. For example:

  type, public :: my_array_t
    integer           :: nlen
    real, allocatable :: values(:)
  end type my_array_t

Intrinsic assignment takes place for such an object in much the same way, e.g.,

  type (my_array_t) :: a
  type (my_array_t) :: b

  ! ... establish some data for b ...

  a = b

However, there are a number of extra steps involved: 1) if the values component of a on the left hand side is already allocated, it is first deallocated; 2) an appropriate allocation is then made for a depending on the size and bounds of b%values(:); 3) all the relevant values of the right-hand side are copied to the left-hand side.

If b%values is unallocated, then step (2) is omitted, and a%values is also unallocated after assignment.

This is, in the jargon, a deep copy. You have a copy of the actual data.

Again, this process is repeated until any ultimate allocatable component is reached.

Derived types as procedure arguments

Recall the behaviour of allocatable arrays as arguments to procedures. When the argument is intent(out) the array is deallocated, the same applies to allocatable components of derived types when a derived-type argument is intent(out).

Pointer components

For types with pointer components, the situation is different. Consider:

  type, public :: my_array_pointer_t
    integer       :: nlen
    real, pointer :: values(:)
  end type my_array_pointer_t

Assignment here means that the pointer becomes associated with the target on the right-hand side.

  type (my_array_pointer_t) :: a
  type (my_array_pointer_t) :: b

  b%nlen = 10
  allocate(b%values(b%nlen))
  a = b

This implies that if, in a subsequent step, the target becomes unassociated (or deallocated), then the reference retained in a is in a bad state.

This is a shallow copy. No data have been duplicated; only the pointer description itself.

Example (5 minutes)

Shallow and deep copies

In the accompanying module my_array_type.f90 both the types above have been declared, along with a function to initialise some array values. Compile the example program:

$ ftn my_array_type.f90 example2.f90

and check the values printed out. What happens if you insert a call to my_array_destroy(a) (which deallocates the values associated with the my_array_t argument) at the end of the program and try to print the values of the pointer type c again?

Solution 1A

The output of the initial program

 State of a            3 T
 State of c            3 T   1.00000000       2.00000000       3.00000000 

Solution 1B

The output of the program after destroying a shows c%values now points to uninitialised data, however c%nlen is a deep copy and is still “valid” (in some sense).

 State of a            3 T
 State of c            3 T   1.00000000       2.00000000       3.00000000    
 State of c            3 T  -1.40913533E-36   1.56146688E-41   1.12103877E-44

What happens if you try to make a direct assignment between a my_array_t object on the right-hand side, and a my_array_pointer_t on the left-hand side?

Solution 2

The compiler rejects the code as invalid due to different types.

gfortran my_array_type.f90 example2.f90 && ./a.out
example2.f90:24:6:

   24 |   c = b
      |      1
Error: Cannot convert TYPE(my_array_t) to TYPE(my_array_pointer_t) at (1)

A defined assignment

If something other than intrinsic assignment for types is required, it is possible to overload the meaning of the assignment operation =.

For example, if an assignment between two objects of my_array_t were required, one could write, as part of a module:

  subroutine my_assignment(a, b)

    type (my_array_t), intent(out) :: a
    type (my_array_t), intent(in)  :: b

    a%nlen = b%nlen
    a%values = b%values

  end subroutine my_assignment

  interface assignment (=)
    module procedure my_assignment
  end interface assignment (=)

This must be a subroutine with two arguments, the first with intent(out) (or inout) to represent the left-hand side of the assignment, and the second with intent(in) to represent the right-hand side.

Exercise (10 minutes)

Constructing a derived type pointer

In example2.f90 we explcitly assigned both components of the my_array_pointer_t in the code and found we could not make an assignment between my_array_pointer_t and my_array_t. Add a subroutine in my_array_type.f90 to make this possible.

Solution

First we define the (=) interface in the module

interface assignment (=)
   module procedure my_array_ptr_assignment
end interface assignment (=)

and the assignemnt subroutine itself

subroutine my_array_ptr_assignment(a, b)

  type(my_array_pointer_t), intent(out) :: a
  type(my_array_t), pointer, intent(in) :: b

  a%nlen = b%nlen
  a%values => b%values

end subroutine my_array_ptr_assignment

noting that the assignment target must be a pointer so that we can use it as the target of the my_array_pointer_t. Then we can use c = a in the example program in place of the member-wise assignment.

Key Points

  • Derived types enable creating custom data structures in Fortran.

  • Access to components of derived types can be controlled via the private attribute