Object Oriented Programming

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Fortran 2023 (ISO/IEC 1539-1:2023, J3/23-007r1) and was checked on 2026-10-19. Every example was compiled warning-free with gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all (GNU Fortran 16.2) and run, and the examples were also built and run with LLVM Flang 23; the output shown is real output, and the compilers agree unless the text says otherwise. Examples that are deliberately wrong, or that expose a difference between the compilers, are labelled as such and show the real diagnostics. The rules are in these places in the standard: the class type specifier is 7.3.2.3 and type compatibility 7.3.3, type extension is 7.5.7 (extensible, extended and abstract types, inheritance, overriding), the structure constructor is 7.5.10, the allocate statement is 9.7.1, intrinsic assignment is 10.2.1.2 and 10.2.1.3, the associate construct is 11.1.3 and the select type construct is 11.1.11. The intrinsic functions are extends_type_of (16.9.86), same_type_as (16.9.177), move_alloc (16.9.147) and storage_size (16.9.200), and the rules for polymorphic actual and dummy arguments are in 15.5.2.4 to 15.5.2.8.

Fortran has had object-oriented features since Fortran 2003. A derived type can be extended into a new type that has all the components and bindings of its parent plus its own, a variable declared with class can hold a value of its declared type or of any extension at run time, and a call through such a variable runs the code of the dynamic type. That is single inheritance with run-time polymorphism, in the same family as the classes of Java or C++ and unlike the trait based design of Rust or the interface satisfaction of Go. This page covers the language mechanisms: extension, polymorphic variables, allocation and assignment, the select type and associate constructs, unlimited polymorphism, heterogeneous containers, and what dynamic dispatch costs. The things that a class definition is made of, components, default initialization and structure constructors, are in Derived Types; parameterized types are in Parameterized Derived Types; type-bound procedures, overriding rules, abstract types, deferred bindings and finalization are in Type-Bound Procedures, Abstract Types and Finalization; and generic bindings and defined operators are in Interfaces, Generics and Operator Overloading.

Vocabulary: Fortran terms for object-oriented ideas

Idea in other languages Fortran construct Notes

class

a derived type, usually in a module, with contains bindings

no separate "class" keyword: type; class is used in declarations of variables

object, instance

a variable of the type (a data object)

objects are values: a type(t) variable is not a reference

method

type-bound procedure, procedure :: name => implementation

the object is passed explicitly as the first dummy argument, self by convention

this, self

the passed-object dummy argument, class(t), intent(in) :: self

it must be polymorphic for an extensible type (7.5.4.5, C765)

inheritance

type, extends(parent) :: child

single inheritance only; the parent type must be extensible

virtual method

any ordinary type-bound procedure

bindings are dispatched on the dynamic type unless declared non_overridable

abstract class, pure virtual method

abstract type, deferred binding

see the next page of this section

interface, trait

an abstract type with only deferred bindings

there is no "implements": a type can extend one parent only

reference or pointer of base type

class(base), allocatable or class(base), pointer

a class variable must be a dummy argument, allocatable or a pointer (C708)

instanceof, down-cast

select type, same_type_as, extends_type_of

there are no unchecked casts

Object, void*, any

class(*), the unlimited polymorphic entity

not type(*), which is assumed-type for C interoperability

constructor

structure constructor t(...), or a generic interface named like the type

covered in Derived Types

destructor

final subroutine

covered in Type-Bound Procedures, Abstract Types and Finalization

private, encapsulation

private in a module or on components

access is controlled by the module, not by the type: there is no protected member

There is no separate namespace for a type’s members and no implicit object: a call call s%describe() is the call describe_source(s) with the object in the first argument position, selected by the binding on the type of s. Mixing the two styles is routine in Fortran code: object-based programming (derived types with bindings, no extension) is common and costs nothing at run time, and object-oriented programming (extension and polymorphism) is used where a set of variants must be chosen at run time. The section Object-based or object-oriented says when each one fits.

Running examples: heat sources and the heatflow solvers

Two hierarchies are used. The first one is small and only exists for this page: a heat source in the plate that heatflow simulates, with a point source as the root and two extensions. The second one is the real hierarchy of heatflow, solver_t and its Jacobi and FTCS extensions, shown in The heatflow solver hierarchy.

classDiagram class heat_source_t { real x real y real power describe() footprint() real } class disc_source_t { real radius footprint() real } class line_source_t { real length real width footprint() real } class moving_disc_t { real vx real vy } heat_source_t <|-- disc_source_t heat_source_t <|-- line_source_t disc_source_t <|-- moving_disc_t

Most examples use this module. The type heat_source_t has an inherited-by-everyone binding describe and a binding footprint that the extensions override; describe calls footprint through self, which is the smallest example of dynamic dispatch: the same line of code, self%footprint(), runs different procedures for different dynamic types.

module heatflow_sources
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: dp, heat_source_t, disc_source_t, line_source_t
  integer, parameter :: dp = real64
  real(dp), parameter :: pi = 3.14159265358979323846_dp

  type :: heat_source_t
    real(dp) :: x = 0.0_dp          ! position in the plate
    real(dp) :: y = 0.0_dp
    real(dp) :: power = 0.0_dp      ! watts
  contains
    procedure :: describe => describe_source
    procedure :: footprint => point_footprint
  end type heat_source_t

  type, extends(heat_source_t) :: disc_source_t
    real(dp) :: radius = 0.1_dp
  contains
    procedure :: footprint => disc_footprint
  end type disc_source_t

  type, extends(heat_source_t) :: line_source_t
    real(dp) :: length = 1.0_dp
    real(dp) :: width = 0.01_dp
  contains
    procedure :: footprint => line_footprint
  end type line_source_t

contains

  subroutine describe_source(self)
    class(heat_source_t), intent(in) :: self
    print '(a, 2f6.2, a, f7.2, a, f8.5)', '  source at', self%x, self%y, '  power', self%power, &
      ' W  footprint', self%footprint()
  end subroutine describe_source

  real(dp) function point_footprint(self) result(area)
    class(heat_source_t), intent(in) :: self
    area = 0.0_dp                     ! an ideal point source covers no area
  end function point_footprint

  real(dp) function disc_footprint(self) result(area)
    class(disc_source_t), intent(in) :: self
    area = pi * self%radius**2
  end function disc_footprint

  real(dp) function line_footprint(self) result(area)
    class(line_source_t), intent(in) :: self
    area = self%length * self%width
  end function line_footprint

end module heatflow_sources

The examples after this one need the module to be compiled first, for instance gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all sources_mod.f90 01_extension.f90, and the listings below start at the first program unit that is not in this module.

Type extension

A derived type is extensible unless it has the sequence or bind(c) attribute or is c_ptr or c_funptr from iso_c_binding (7.5.7.1). Writing extends(parent) in the type statement creates an extended type. Its parent type is the one named; an extension of a type is the type itself or any type that extends it, directly or through other types. A type has one parent at most, so the extension relation forms a tree with extensible types at the roots.

An extended type inherits (7.5.7.2):

  • all the type parameters and components of the parent (and of its ancestors), with the attributes they had, and

  • all the type-bound procedures of the parent that it does not override.

It may add components and bindings of its own, and it has a parent component: a scalar, nonpointer, nonallocatable component whose name is the name of the parent type and whose type is the parent type. The inherited components are addressable directly (disc%x) or through the parent component (disc%heat_source_t%x). A component or type parameter of the extension cannot have the same name as an accessible one of its parent. If the extended type is defined in a module, the parent component has the accessibility that the parent type has in the module that defines the parent (7.5.7.2).

The program below builds one value of each type of the module above, uses both ways to address an inherited component, calls an inherited binding and an overridden binding on every type, and copies the parent component into a variable of the parent type.

program type_extension
  use heatflow_sources
  implicit none
  type(heat_source_t) :: plain
  type(disc_source_t) :: disc
  type(line_source_t) :: line

  plain = heat_source_t(0.25_dp, 0.75_dp, 5.0_dp)

  ! The structure constructor of an extended type takes the parent component first ...
  disc = disc_source_t(heat_source_t(0.5_dp, 0.5_dp, 20.0_dp), 0.05_dp)
  ! ... or the parent component by keyword, followed by the new components.
  line = line_source_t(heat_source_t = heat_source_t(0.1_dp, 0.9_dp, 8.0_dp), length = 0.4_dp, width = 0.02_dp)

  ! The inherited components may also be set by keyword.
  disc = disc_source_t(x = 0.5_dp, y = 0.5_dp, power = 20.0_dp, radius = 0.05_dp)

  ! Inherited components are addressed directly, or through the parent component.
  print '(a, f6.2, a, f6.2)', 'disc%x =', disc%x, '   disc%heat_source_t%x =', disc%heat_source_t%x
  disc%heat_source_t%power = disc%power * 1.5_dp
  print '(a, f6.2, a, f6.2)', 'disc%radius =', disc%radius, '   power after scaling =', disc%power

  ! The inherited binding describe works on every extension; footprint is overridden.
  call plain%describe()
  call disc%describe()
  call line%describe()

  ! The parent component is an ordinary variable of the parent type.
  plain = disc%heat_source_t
  print '(a, f6.2, a, f8.5)', 'plain after copying the parent component: power =', plain%power, &
    '  footprint', plain%footprint()
  print '(a, i0, a, i0, a, i0)', 'storage_size in bits: parent ', storage_size(plain), ', disc ', &
    storage_size(disc), ', line ', storage_size(line)
end program type_extension

Output:

disc%x =  0.50   disc%heat_source_t%x =  0.50
disc%radius =  0.05   power after scaling = 30.00
  source at  0.25  0.75  power   5.00 W  footprint 0.00000
  source at  0.50  0.50  power  30.00 W  footprint 0.00785
  source at  0.10  0.90  power   8.00 W  footprint 0.00800
plain after copying the parent component: power = 30.00  footprint 0.00000
storage_size in bits: parent 192, disc 256, line 320

Four details are worth knowing:

  • Structure constructors take the parent component first. disc_source_t(heat_source_t(...), 0.05_dp) gives the parent component, a value of the parent type, followed by the components the extension adds. The parent component can also be given by keyword, which is the type name of the parent: heat_source_t = heat_source_t(...). The inherited components are components of the extended type too, so each one can be given by keyword (disc_source_t(x = 0.5_dp, ..., radius = 0.05_dp) in the program above). A constructor cannot give both the parent component and one of its inherited components, and a component without default initialization must be given (7.5.10, C7103 to C7107). Positional values always start with the parent component, never with the inherited component x.

  • storage_size reports the whole object. The 192 bits of the parent are three 64-bit reals, and each extension adds its own components to that; the 256 and 320 bits printed above are the sizes of disc_source_t and line_source_t. The hidden information that identifies the dynamic type of a polymorphic variable is not counted, because a compiler is free to keep it in the variable rather than in the object. The standard fixes the behaviour, not the layout (the sketch in Polymorphic entities: class shows one common arrangement).

  • Extension is an is-a relation, not a union. plain = disc%heat_source_t is an ordinary assignment of the parent part. Assigning disc itself to a type(heat_source_t) variable does not compile: derived-type intrinsic assignment requires the same declared type (Table 10.8 in 10.2.1.2), and GNU Fortran 16.2 says "Cannot convert TYPE(disc_source_t) to TYPE(heat_source_t)", while Flang 23 says that no intrinsic or user-defined assignment matches the operand types. Section Polymorphic assignment and slicing shows the assignments that are allowed.

  • Overriding is checked by the compiler. An overriding binding must have the same name, the same dummy arguments (names and characteristics) except for the passed-object dummy, which is class(disc_source_t) instead of class(heat_source_t), and the same result characteristics, and it must not be less accessible than the overridden one (7.5.7.3). The rules, non_overridable, and abstract bindings are the topic of Type-Bound Procedures, Abstract Types and Finalization.

What cannot be extended or redeclared

The following program is deliberately non-conforming: it tries to extend a sequence type, to extend a bind(c) type and to redeclare a component that the parent already has.

! INTENTIONALLY NON-CONFORMING: extending a sequence type and a bind(c) type, and redeclaring a parent component.
module bad_extension
  use, intrinsic :: iso_c_binding, only: c_double
  implicit none
  type :: packed_t
    sequence
    real :: a
  end type packed_t
  type, bind(c) :: c_point_t
    real(c_double) :: x, y
  end type c_point_t

  type, extends(packed_t) :: from_sequence
    real :: b
  end type from_sequence

  type, extends(c_point_t) :: from_bind_c
    real(c_double) :: z
  end type from_bind_c

  type :: base_t
    real :: x
  end type base_t
  type, extends(base_t) :: dup_t
    real :: x
  end type dup_t
end module bad_extension

Diagnostics from gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all; the two "Expecting END MODULE" messages are follow-on errors that the compiler produces after rejecting an extends clause, and are not separate mistakes:

02_extension_errors.f90:13:26:

   13 |   type, extends(packed_t) :: from_sequence
      |                          1
Error: 'packed_t' cannot be extended at (1) because it is a SEQUENCE type
02_extension_errors.f90:15:5:

   15 |   end type from_sequence
      |     1
Error: Expecting END MODULE statement at (1)
02_extension_errors.f90:17:27:

   17 |   type, extends(c_point_t) :: from_bind_c
      |                           1
Error: 'c_point_t' cannot be extended at (1) because it is BIND(C)
02_extension_errors.f90:19:5:

   19 |   end type from_bind_c
      |     1
Error: Expecting END MODULE statement at (1)
02_extension_errors.f90:25:13:

   21 |   type :: base_t
      |                2
......
   25 |     real :: x
      |             1
Error: Component 'x' at (1) already in the parent type at (2)

Diagnostics from LLVM Flang 23 for the same file:

error: Semantic errors in 02_extension_errors.f90
02_extension_errors.f90:13:30: error: The parent type is not extensible
    type, extends(packed_t) :: from_sequence
                               ^^^^^^^^^^^^^
02_extension_errors.f90:17:31: error: The parent type is not extensible
    type, extends(c_point_t) :: from_bind_c
                                ^^^^^^^^^^^
02_extension_errors.f90:25:13: error: Component 'x' is already declared in a parent of this derived type
      real :: x
              ^
02_extension_errors.f90:22:13: Previous declaration of 'x'
      real :: x
              ^

Types meant for C interoperability (bind(c)) or for storage association (sequence) have a fixed layout that must not carry hidden type information, which is why they cannot take part in extension; Interoperability with C shows how to wrap a bind(c) type in an ordinary type when an object-oriented interface is wanted.

Private components and extension

Accessibility is a property of the module, so extension across modules interacts with private. A type’s private components are inherited by its extensions but stay inaccessible outside the module that defines the parent (7.5.7.2, NOTE 1). The binding of the parent, which is defined in the parent’s module, can still use them. The two modules below define a sensor with a private calibration offset, extend it in another module and use the extension from the main program. The offset has a default value, so it may be left out of the structure constructor, and the inherited binding calibrated still uses it.

module sensors_base
  implicit none
  private
  public :: sensor_t

  type :: sensor_t
    integer :: id = 0                     ! public component
    real :: reading = 0.0                 ! public component
    real, private :: offset = 0.5         ! private: invisible outside this module, but part of every extension
  contains
    procedure :: calibrated => sensor_calibrated
  end type sensor_t

contains
  real function sensor_calibrated(self) result(r)
    class(sensor_t), intent(in) :: self
    r = self%reading - self%offset
  end function sensor_calibrated
end module sensors_base

module sensors_probe
  use sensors_base, only: sensor_t
  implicit none
  private
  public :: probe_t

  type, extends(sensor_t) :: probe_t
    real :: depth = 0.0
  end type probe_t
end module sensors_probe

program private_components
  use sensors_base, only: sensor_t
  use sensors_probe, only: probe_t
  implicit none
  type(probe_t) :: p
  ! The private component keeps its default initialization, so it may be left out of the constructor.
  p = probe_t(sensor_t(7, 20.0), 1.5)
  print '(a, f5.2)', 'calibrated (uses the private offset through the inherited binding): ', p%calibrated()
  ! print *, p%offset                    ! error: offset is private to sensors_base
end program private_components

Output:

calibrated (uses the private offset through the inherited binding): 19.50

The main program of that example names the type sensor_t itself, so it has to use it from sensors_base: the structure constructor needs the type name to be accessible in the scoping unit (C7107). Naming the private component from outside the module is rejected, by both compilers (the two modules of the previous listing are compiled first, and the main program of that listing is not needed):

! INTENTIONALLY NON-CONFORMING: the private component and the structure constructor of an extension.
program bad_private
  use sensors_probe
  implicit none
  type(probe_t) :: p
  print *, p%offset
end program bad_private

Diagnostics from GNU Fortran 16.2, then LLVM Flang 23:

20_private_errors.f90:6:19:

    6 |   print *, p%offset
      |                   1
Error: Component 'offset' at (1) is a PRIVATE component of 'sensor_t'
error: Semantic errors in 20_private_errors.f90
20_private_errors.f90:6:14: error: PRIVATE name 'offset' is accessible only within module 'sensors_base'
    print *, p%offset
               ^^^^^^

Polymorphic entities: class

A variable declared class(t), where t is an extensible type, is polymorphic (7.3.2.3). It has two types:

  • its declared type t, fixed in the source, which decides which components, bindings and arguments are valid, and

  • its dynamic type, which is t or any extension of t, and which can change during execution.

The dynamic type is determined as follows (7.3.2.3):

  • of an allocated allocatable polymorphic variable, the type it was allocated with;

  • of an associated polymorphic pointer, the dynamic type of its target;

  • of a nonallocatable nonpointer polymorphic dummy argument, the dynamic type of its actual argument;

  • of an unallocated allocatable variable or a disassociated pointer, its declared type;

  • of an associating entity of associate or select type, the dynamic type of the selector;

  • of an entity that is not polymorphic, its declared type.

A class entity must be a dummy argument or have the allocatable or pointer attribute (C708): a plain local variable has a single type for its whole life and gains nothing from being polymorphic, and the compiler needs a descriptor to hold the dynamic type. The type named in class must be extensible (C705).

Declared type, dynamic type and dispatch of a polymorphic variable: the variable s of declared type heat_source_t points to an object with the parent component and the extension part, and its type link leads to the type descriptor of disc_source_t, which lists the bindings of the dynamic type and extends the descriptor of heat_source_t

The figure shows one common arrangement; the standard prescribes the observable behaviour (which binding runs, which type guard matches), not the layout. Type compatibility (7.3.3) is the rule that says what may be associated with what: a nonpolymorphic entity is type compatible only with entities of the same declared type, a polymorphic one with entities of its declared type or any extension, and an unlimited polymorphic one with all entities. The relation is not symmetric: class(heat_source_t) is type compatible with a class(disc_source_t) entity, but not the other way round.

The next program declares an allocatable and a pointer of declared type heat_source_t, gives them values of different dynamic types, and prints what same_type_as and extends_type_of report for the dynamic type inside a polymorphic dummy argument. same_type_as(a, b) is true when the dynamic types of both are the same extensible type; extends_type_of(a, mold) is true when the dynamic type of a is the dynamic type of mold or an extension of it (16.9.177 and 16.9.86). Both look only at the dynamic type, never at the value, so a variable with a disassociated pointer or an unallocated allocatable can be used as the mold.

program polymorphic_variables
  use heatflow_sources
  implicit none
  type(heat_source_t) :: point_mold
  type(disc_source_t) :: disc_mold
  type(disc_source_t), target :: stove
  class(heat_source_t), allocatable :: s
  class(heat_source_t), pointer :: p

  ! An unallocated allocatable: the dynamic type is the declared type.
  print '(a, l1)', 'unallocated:  same_type_as(s, point_mold) = ', same_type_as(s, point_mold)

  allocate (disc_source_t :: s)               ! typed allocation: dynamic type disc_source_t
  call inspect('allocated as disc_source_t', s)

  deallocate (s)
  allocate (s, source=heat_source_t(0.2_dp, 0.2_dp, 3.0_dp))
  call inspect('reallocated as heat_source_t', s)

  stove = disc_source_t(heat_source_t(0.5_dp, 0.5_dp, 40.0_dp), 0.15_dp)
  p => stove                                   ! a polymorphic pointer takes the dynamic type of its target
  call inspect('pointer to a disc_source_t', p)
  call p%describe()

  nullify (p)                                  ! disassociated: dynamic type is the declared type again
  print '(a, l1)', 'disassociated: same_type_as(p, point_mold) = ', same_type_as(p, point_mold)

contains

  ! A polymorphic dummy argument: the declared type is heat_source_t, the dynamic type is the actual's.
  subroutine inspect(label, src)
    character(len=*), intent(in) :: label
    class(heat_source_t), intent(in) :: src
    print '(a, a)', label, ':'
    print '(a, l1, a, l1)', '  same_type_as(src, point_mold) = ', same_type_as(src, point_mold), &
      '   same_type_as(src, disc_mold) = ', same_type_as(src, disc_mold)
    print '(a, l1, a, l1)', '  extends_type_of(src, point_mold) = ', extends_type_of(src, point_mold), &
      '   extends_type_of(src, disc_mold) = ', extends_type_of(src, disc_mold)
    print '(a, i0)', '  storage_size(src) in bits = ', storage_size(src)
  end subroutine inspect

end program polymorphic_variables

Output:

unallocated:  same_type_as(s, point_mold) = T
allocated as disc_source_t:
  same_type_as(src, point_mold) = F   same_type_as(src, disc_mold) = T
  extends_type_of(src, point_mold) = T   extends_type_of(src, disc_mold) = T
  storage_size(src) in bits = 256
reallocated as heat_source_t:
  same_type_as(src, point_mold) = T   same_type_as(src, disc_mold) = F
  extends_type_of(src, point_mold) = T   extends_type_of(src, disc_mold) = F
  storage_size(src) in bits = 192
pointer to a disc_source_t:
  same_type_as(src, point_mold) = F   same_type_as(src, disc_mold) = T
  extends_type_of(src, point_mold) = T   extends_type_of(src, disc_mold) = T
  storage_size(src) in bits = 256
  source at  0.50  0.50  power  40.00 W  footprint 0.07069
disassociated: same_type_as(p, point_mold) = T

Note the first and last lines: before the allocation and after the pointer is disassociated, the dynamic type is the declared type, so same_type_as against a heat_source_t value is true. It is not an error to ask, but it means that a polymorphic entity that is not allocated cannot be told apart from one holding the base type by these two functions; test allocated or associated first. extends_type_of is true when the types are the same (a type is an extension of itself), which is why it reported true for disc_mold when the dynamic type was disc_source_t.

The standard notes (16.9.86, NOTE 2) that extends_type_of is not the same test as the type guard class is in a select type construct: the intrinsic ignores kind type parameters. Both are inquiry tools; ordinary code should prefer a type-bound procedure over asking for the type, and use select type when it needs the extension’s components.

Where a polymorphic entity may and may not appear

The following module is deliberately non-conforming. It declares a polymorphic local variable that is not allocatable, refers to a component of an extension through a variable declared with the parent type, and passes a polymorphic actual argument to a nonpolymorphic dummy argument of a different declared type.

! INTENTIONALLY NON-CONFORMING: where a polymorphic entity may and may not be declared,
! and what a polymorphic entity may do.
module bad_polymorphism
  implicit none
  type :: base_t
    real :: x = 0.0
  end type base_t
  type, extends(base_t) :: child_t
    real :: y = 0.0
  end type child_t
contains
  subroutine plain_local()
    class(base_t) :: c                  ! error: neither dummy, allocatable nor pointer
    print *, c%x
  end subroutine plain_local

  subroutine use_child_component(b)
    class(base_t), intent(in) :: b
    print *, b%y                        ! error: y is not a component of the declared type
  end subroutine use_child_component

  subroutine pass_to_type_dummy(c)
    class(child_t), intent(in) :: c
    call take_base(c)                   ! error: type(base_t) dummy, class(child_t) actual
  end subroutine pass_to_type_dummy

  subroutine take_base(b)
    type(base_t), intent(in) :: b
    print *, b%x
  end subroutine take_base
end module bad_polymorphism

Diagnostics from gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all, in the order the compiler reported them:

04_declaration_errors.f90:19:17:

   19 |     print *, b%y                        ! error: y is not a component of the declared type
      |                 1
Error: 'y' at (1) is not a member of the 'base_t' structure
04_declaration_errors.f90:13:22:

   13 |     class(base_t) :: c                  ! error: neither dummy, allocatable nor pointer
      |                      1
Error: CLASS variable 'c' at (1) must be dummy, allocatable or pointer
04_declaration_errors.f90:24:90:

   24 |     call take_base(c)                   ! error: type(base_t) dummy, class(child_t) actual
      |                                                                                          1
Error: Type mismatch in argument 'b' at (1); passed CLASS(child_t) to TYPE(base_t)

Diagnostics from LLVM Flang 23:

error: Semantic errors in 04_declaration_errors.f90
04_declaration_errors.f90:13:22: error: CLASS entity 'c' must be a dummy argument, allocatable, or object pointer
      class(base_t) :: c                  ! error: neither dummy, allocatable nor pointer
                       ^
04_declaration_errors.f90:19:16: error: Component 'y' not found in derived type 'base_t'
      print *, b%y                        ! error: y is not a component of the declared type
                 ^
04_declaration_errors.f90:5:11: Declaration of derived type 'base_t'
    type :: base_t
            ^^^^^^
04_declaration_errors.f90:24:20: error: Actual argument type 'CLASS(child_t)' is not compatible with dummy argument type 'base_t'
      call take_base(c)                   ! error: type(base_t) dummy, class(child_t) actual
                     ^

The second error is the fundamental discipline of polymorphism: through a class(base) variable only the components and bindings of base exist. Reaching y of an extension needs select type (see select type). The third shows that a polymorphic actual argument may be passed to a nonpolymorphic dummy argument only when the declared types are the same: the dummy then becomes argument associated with the declared-type part of the actual (15.5.2.4), which slices the extension off. To pass the parent part of a class(child_t) variable, pass its parent component: c%base_t.

Typed and sourced allocation

A polymorphic allocatable variable gets its dynamic type from the allocate statement. There are three forms (9.7.1):

Form Dynamic type and value Notes

allocate (x)

the declared type of x, default values

allowed unless x is unlimited polymorphic or of an abstract type (C937)

allocate (disc_source_t :: x)

the type named, default values

typed allocation: the type must be one with which x is type compatible (C938)

allocate (x, source=y)

the dynamic type and the value of y

sourced allocation, a polymorphic clone; Fortran 2003

allocate (x, mold=y)

the dynamic type of y, and its shape if it is an array, but not its value

Fortran 2008; the value of y need not be defined, so a disassociated pointer or an unallocated variable cannot be used, but an uninitialized one can

Only one of source= and a type specification can appear (C949), and the object being allocated has to be type compatible with the source (C950). With source=, if the allocated object is not polymorphic and the source is polymorphic with a dynamic type that differs from its declared type, the value provided is the ancestor component of the source that has the declared type of the object, which is the slicing rule again (9.7.1.2).

The program below allocates four variables, a by typed allocation, b by source=, c by mold= and d by polymorphic assignment (the next section), and prints what each holds. The function clone is the idiom for copying a polymorphic dummy argument: a function with an allocatable polymorphic result that is allocated with source= the argument.

program allocation_and_assignment
  use heatflow_sources
  implicit none
  type(disc_source_t) :: template
  type(heat_source_t) :: plain
  class(heat_source_t), allocatable :: a, b, c, d

  template = disc_source_t(heat_source_t(0.5_dp, 0.5_dp, 12.0_dp), 0.2_dp)

  allocate (disc_source_t :: a)                 ! type-spec: default component values only
  allocate (b, source=template)                 ! source=: dynamic type AND value of template
  allocate (c, mold=template)                   ! mold=: dynamic type only, the value is not copied
  d = template                                  ! polymorphic assignment on an unallocated variable

  print '(a)', 'after allocation:'
  call show('a (type-spec)', a)
  call show('b (source=)', b)
  call show('c (mold=)', c)
  call show('d (assignment)', d)

  ! Assignment to an allocated polymorphic variable reallocates it when the dynamic type differs.
  d = heat_source_t(0.9_dp, 0.1_dp, 1.0_dp)
  print '(a)', 'after d = heat_source_t(...):'
  call show('d', d)
  d = clone(b)
  print '(a)', 'after d = clone(b):'
  call show('d', d)

  ! Assignment to a nonpolymorphic variable copies only the part of the declared type: the extension is sliced off.
  plain = b
  print '(a, f6.2, a, f8.5)', 'plain = b: power =', plain%power, '  footprint (parent) ', plain%footprint()

contains

  function clone(src) result(copy)
    class(heat_source_t), intent(in) :: src
    class(heat_source_t), allocatable :: copy
    allocate (copy, source=src)                 ! the idiom for a polymorphic copy
  end function clone

  subroutine show(label, src)
    character(len=*), intent(in) :: label
    class(heat_source_t), intent(in) :: src
    print '(2x, a, t18, a, l1, a, f6.2, a, f8.5)', label, 'is a disc: ', same_type_as(src, template), &
      '  power', src%power, '  footprint', src%footprint()
  end subroutine show

end program allocation_and_assignment

Output:

after allocation:
  a (type-spec)  is a disc: T  power  0.00  footprint 0.03142
  b (source=)    is a disc: T  power 12.00  footprint 0.12566
  c (mold=)      is a disc: T  power  0.00  footprint 0.03142
  d (assignment) is a disc: T  power 12.00  footprint 0.12566
after d = heat_source_t(...):
  d              is a disc: F  power  1.00  footprint 0.00000
after d = clone(b):
  d              is a disc: T  power 12.00  footprint 0.12566
plain = b: power = 12.00  footprint (parent)  0.00000

The variable c allocated with mold=template has the dynamic type of template but shows the default values of the type (power 0.00), not those of the mold. The standard says only that the value of the mold need not be defined; code should not rely on what the allocated object holds before it is assigned. If the value is needed, use source=.

Allocating a variable that is already allocated is an error condition that is detected at run time. With a stat= specifier the program continues; without one it terminates. The status codes are processor dependent, which the output of the next program shows: GNU Fortran 16.2 reported 5014 and Flang 23 reported 12 for the same event, and the errmsg= texts differ too. The last two statements of the program use an unallocated polymorphic variable as the object of a call, which -fcheck=all catches; the statement has no defined behaviour without the check.

! Run-time errors with polymorphic objects: allocation status.
program runtime_errors
  use heatflow_sources
  implicit none
  class(heat_source_t), allocatable :: s
  integer :: stat
  character(len=100) :: msg

  allocate (disc_source_t :: s)
  allocate (line_source_t :: s, stat=stat, errmsg=msg)      ! already allocated: an error condition
  print '(a, i0)', 'stat = ', stat
  print '(a, a)', 'errmsg = ', trim(msg)
  print '(a, l1)', 'still a disc: ', same_type_as(s, disc_source_t())

  deallocate (s)
  call s%describe()                                           ! s is not allocated
end program runtime_errors

Output with GNU Fortran 16.2 and -fcheck=all; the lines before At line are the output of the program:

At line 16 of file 22_runtime_errors.f90
Fortran runtime error: Allocatable actual argument 's' is not allocated

Error termination. Backtrace:
stat = 5014
errmsg = Attempt to allocate an allocated object
still a disc: T
#0  0xffffad7a196f in ???
#1  0xffffad7a26b3 in ???
#2  0xffffad7a2c13 in ???
#3  0x40136b in runtime_errors
	at /tmp/o/22_runtime_errors.f90:16
#4  0x4013cf in main
	at /tmp/o/22_runtime_errors.f90:3

The addresses in the backtrace and the status code depend on the compiler and the machine. With Flang 23, a build of the program without the last two statements printed stat = 12, errmsg = Base address is not null and still a disc: T; the complete program with the call on the unallocated variable ended with a segmentation fault and no diagnostic. A program must test allocated before using an allocatable object, and must not depend on a compiler option to report the mistake.

Polymorphic assignment and slicing

Intrinsic assignment to a polymorphic variable has stricter rules than for ordinary variables (10.2.1.2):

  • if the variable is polymorphic it must be allocatable (and not a coarray); assignment to a polymorphic dummy argument or pointer is not an intrinsic assignment;

  • it must be type compatible with the expression, which, for a polymorphic variable, means the expression’s declared type is the variable’s declared type or an extension of it;

  • when the variable is allocated and its dynamic type differs from that of the expression, it is deallocated and reallocated with the dynamic type of the expression (10.2.1.3); if it is unallocated it is allocated with that dynamic type.

That is what happened to d in the program of the previous section: d = template allocated d as a disc_source_t with the value of template; d = heat_source_t(...) deallocated the disc and reallocated d as a plain heat_source_t; and d = clone(b) made it a disc again. Assigning a polymorphic expression to a nonpolymorphic variable is allowed when the declared types agree, and it copies the part of the declared type only (the last line of that program, plain = b, kept the 12 W of power and lost the radius). That slicing is silent. Rules (1) and (4) are violated by the following module, which is deliberately non-conforming:

! INTENTIONALLY NON-CONFORMING: intrinsic assignment rules for polymorphic variables.
module bad_assignment
  use heatflow_sources
  implicit none
contains
  subroutine overwrite(dst, src)
    class(heat_source_t), intent(inout) :: dst      ! polymorphic but not allocatable
    class(heat_source_t), intent(in) :: src
    dst = src                                         ! error: variable is polymorphic and not allocatable
  end subroutine overwrite

  subroutine narrow(src)
    class(heat_source_t), intent(in) :: src
    class(disc_source_t), allocatable :: d
    d = src                                           ! error: class(heat_source_t) is not type compatible with class(disc_source_t)
  end subroutine narrow
end module bad_assignment

Diagnostics from GNU Fortran 16.2, then LLVM Flang 23:

06_assignment_errors.f90:15:8:

   15 |     d = src                                           ! error: class(heat_source_t) is not type compatible with class(disc_source_t)
      |        1
Error: Cannot convert CLASS(heat_source_t) to CLASS(disc_source_t) at (1)
06_assignment_errors.f90:9:5:

    9 |     dst = src                                         ! error: variable is polymorphic and not allocatable
      |     1~~~
Error: Nonallocatable variable must not be polymorphic in intrinsic assignment at (1) - check that there is a matching specific subroutine for '=' operator
error: Semantic errors in 06_assignment_errors.f90
06_assignment_errors.f90:9:5: error: Left-hand side of intrinsic assignment may not be polymorphic unless assignment is to an entire allocatable
      dst = src                                         ! error: variable is polymorphic and not allocatable
      ^^^
06_assignment_errors.f90:15:5: error: No intrinsic or user-defined ASSIGNMENT(=) matches operand types CLASS(disc_source_t) and CLASS(heat_source_t)
      d = src                                           ! error: class(heat_source_t) is not type compatible with class(disc_source_t)
      ^^^^^^^

When a polymorphic dummy argument must be overwritten, there are three possibilities: make the dummy allocatable, give the type a defined assignment through a generic assignment(=) binding (see Interfaces, Generics and Operator Overloading), or add a type-bound copy_from procedure that uses select type.

Polymorphic assignment was added to Fortran by the 2008 standard, together with allocate (x) for a polymorphic variable without a type specification (which allocates the declared type), according to the list of Fortran 2008 features in the GNU Fortran wiki; GNU Fortran 16.2 and Flang 23 both implement it.

Deep copies of polymorphic components

A derived type that contains an allocatable polymorphic component is copied by intrinsic assignment as if each component were assigned in turn, and for an allocatable component the rule of 10.2.1.3 is: deallocate the component of the variable if it is allocated; if the component of the expression is allocated, allocate the component of the variable with the same dynamic type and type parameters and assign the value, using defined assignment if the declared type of the component has one and intrinsic assignment for the dynamic type otherwise. The heterogeneous list in Heterogeneous containers relies on this: copy = list copies every slot, including the dynamic type of each polymorphic item. A pointer component is pointer assigned instead, so a copy shares what it points to.

Polymorphic dummy arguments, results and pointers

A dummy argument declared class(t) accepts an actual argument of declared type t or any extension. Which declarations of dummy and actual are valid depends on the attributes (15.5.2):

Dummy argument Actual argument Rule

class(t), no allocatable or pointer

t, class(t) or any extension, polymorphic or not

type compatible; the dummy has the dynamic type of the actual (15.5.2.4)

type(t)

type(t) or class(t)

the same declared type is required; the dummy gets the declared-type part (slicing)

class(t), allocatable or class(t), pointer

class(t) with the same declared type

an extension is not accepted, because the callee could allocate a different dynamic type (15.5.2.6)

type(t), allocatable or type(t), pointer

nonpolymorphic type(t)

the callee has no place to record a dynamic type (15.5.2.6, NOTE)

class(*)

any entity

the dummy is unlimited polymorphic; the effective argument keeps its dynamic type

The factory pattern follows from this: a procedure with an intent(out) allocatable polymorphic dummy argument, or a function with an allocatable polymorphic result, chooses the dynamic type and hands back an object that the caller treats through the declared type. The program below has a subroutine make_source that allocates one of three types according to a name, and a type(heat_source_t) dummy argument that shows the slicing of its polymorphic actual argument.

module source_factory
  use heatflow_sources
  implicit none
contains

  ! Output factory: the dummy is polymorphic and allocatable, so the callee chooses the dynamic type.
  subroutine make_source(kind, s)
    character(len=*), intent(in) :: kind
    class(heat_source_t), allocatable, intent(out) :: s
    select case (kind)
    case ('disc')
      allocate (disc_source_t :: s)
    case ('line')
      allocate (line_source_t :: s)
    case default
      allocate (heat_source_t :: s)
    end select
  end subroutine make_source

  ! A nonpolymorphic dummy receives only the declared-type part of its actual argument.
  subroutine parent_part(p)
    type(heat_source_t), intent(in) :: p
    print '(a, f6.2, a, f8.5)', '  parent_part: power', p%power, '   footprint of the parent part', p%footprint()
  end subroutine parent_part

end module source_factory

program dummy_arguments
  use heatflow_sources
  use source_factory
  implicit none
  class(heat_source_t), allocatable :: s
  character(len=4), parameter :: kinds(3) = [character(len=4) :: 'disc', 'line', 'dot']
  integer :: i

  do i = 1, size(kinds)
    call make_source(kinds(i), s)
    s%power = 10.0_dp * i
    print '(a, a, f8.5)', trim(kinds(i)), ': footprint through the dynamic type ', s%footprint()
    ! The actual argument is class(heat_source_t); the dummy is type(heat_source_t): same declared type, so allowed.
    call parent_part(s)
  end do
end program dummy_arguments

Output:

disc: footprint through the dynamic type  0.03142
  parent_part: power 10.00   footprint of the parent part 0.00000
line: footprint through the dynamic type  0.01000
  parent_part: power 20.00   footprint of the parent part 0.00000
dot: footprint through the dynamic type  0.00000
  parent_part: power 30.00   footprint of the parent part 0.00000

The output has a point of interest: the footprint of the line object computed through its dynamic type (0.01000) becomes 0.00000 when the same object is received by parent_part, whose dummy argument is nonpolymorphic, because dispatch now starts from the declared type heat_source_t. An intent(out) allocatable dummy argument is deallocated on entry, so the factory always starts from an unallocated object and the previous dynamic type is irrelevant (15.5.2.7).

Passing an extension to an allocatable polymorphic dummy argument is a different story, and the compilers reject it. The following module is deliberately non-conforming:

! INTENTIONALLY NON-CONFORMING: allocatable and pointer dummy arguments need the same declared type (15.5.2.6).
module bad_factory
  use heatflow_sources
  implicit none
contains
  subroutine make_source(s)
    class(heat_source_t), allocatable, intent(out) :: s
    allocate (disc_source_t :: s)
  end subroutine make_source

  subroutine caller()
    class(disc_source_t), allocatable :: d
    call make_source(d)                    ! error: declared types differ, although disc_source_t extends heat_source_t
  end subroutine caller
end module bad_factory

Diagnostics from GNU Fortran 16.2, then LLVM Flang 23:

24_dummy_errors.f90:13:21:

   13 |     call make_source(d)                    ! error: declared types differ, although disc_source_t extends heat_source_t
      |                     1
Error: Actual argument to 's' at (1) must have the same declared type
error: Semantic errors in 24_dummy_errors.f90
24_dummy_errors.f90:13:22: error: POINTER or ALLOCATABLE dummy and actual arguments must have the same declared type and kind
      call make_source(d)                    ! error: declared types differ, although disc_source_t extends heat_source_t
                       ^

Polymorphic pointers

A polymorphic pointer takes the dynamic type of its target (7.3.2.3), so pointing it at a different target changes its dynamic type. Pointer assignment needs the target to be type compatible with the pointer (C1016 in 10.2.2): upward in the hierarchy it is always allowed, downward only through select type, where the associating entity of a type is block has the extension’s type and, because the selector is a pointer, the target attribute. A pointer can be unlimited polymorphic and then accepts any target. Pointer basics are in Pointers and Targets.

program polymorphic_pointers
  use heatflow_sources
  implicit none
  type(disc_source_t), target :: stove
  type(line_source_t), target :: heater
  class(heat_source_t), pointer :: any_source
  type(disc_source_t), pointer :: as_disc
  class(*), pointer :: anything

  stove = disc_source_t(heat_source_t(0.5_dp, 0.5_dp, 30.0_dp), 0.1_dp)
  heater = line_source_t(heat_source_t(0.2_dp, 0.8_dp, 5.0_dp), 0.5_dp, 0.02_dp)

  any_source => stove                    ! up the hierarchy: always allowed
  anything => heater                     ! class(*) pointer accepts any target
  ! as_disc => any_source                ! not allowed: a nonpolymorphic pointer needs a TYPE IS block

  select type (any_source)
  type is (disc_source_t)
    as_disc => any_source                ! allowed: inside TYPE IS the associating entity is type(disc_source_t)
    as_disc%radius = 0.25_dp             ! changes stove
  end select
  print '(a, f5.2, a, f5.2)', 'stove%radius', stove%radius, '   as_disc%radius', as_disc%radius

  select type (anything)
  class is (heat_source_t)
    print '(a, f6.2)', 'anything is a heat source with power', anything%power
  end select

  any_source => heater                   ! retargeting changes the dynamic type
  call any_source%describe()
  print '(a, l1)', 'associated(any_source, heater): ', associated(any_source, heater)
  print '(a, l1)', 'associated(any_source, stove):  ', associated(any_source, stove)
end program polymorphic_pointers

Output:

stove%radius 0.25   as_disc%radius 0.25
anything is a heat source with power  5.00
  source at  0.20  0.80  power   5.00 W  footprint 0.01000
associated(any_source, heater): T
associated(any_source, stove):  F

select type

select type selects one block according to the dynamic type of a polymorphic expression (11.1.11). Its syntax and constraints are:

Rule Statement

selector

select type ([name => ] selector); the selector must be polymorphic (C1164), and if it is not a named variable the form with name => is required (C1162)

type is (t)

matches when the dynamic type and kind parameters equal t; t may be an intrinsic type (for example integer(int32)) if the selector is unlimited polymorphic

class is (t)

matches when the dynamic type is t or an extension of t; t must be a derived type

class default

matches when nothing else does; the associating entity has the declared type of the selector

extension rule

without an unlimited polymorphic selector each type is and class is type must be an extension of the declared type of the selector (C1167)

uniqueness

each type may appear in at most one type is and at most one class is guard, and there is at most one class default (C1168 and C1169)

length parameters

a type guard of a type with length type parameters must make each of them assumed, as character(len=*) (C1165)

sequence, bind(c)

types with these attributes cannot appear in a type guard (C1166)

Inside a block, the associating entity (the associate name, or the selector’s own name if none is given) is an alias of the selector. Its type depends on the guard (11.1.11.2): in a type is block it is not polymorphic and has the guard type; in a class is block it is polymorphic with the guard type as its declared type; in class default it is polymorphic with the selector’s declared type.

The guards are not tried in source order. The standard (11.1.11.2, NOTE 1) says the construct "selects the most particular type guard": a matching type is wins over every class is, among the matching class is guards the one for the most extended type wins, and class default is used only if no guard matches.

flowchart TD A["select type: evaluate the selector,
take its dynamic type"] --> B{"a TYPE IS guard names
exactly that type?"} B -- yes --> T["run that block
associating entity: nonpolymorphic"] B -- no --> C{"one or more CLASS IS guards
name that type or an ancestor?"} C -- yes --> D["run the guard for the most extended type
associating entity: polymorphic"] C -- no --> E{"CLASS DEFAULT present?"} E -- yes --> F["run it
declared type of the selector"] E -- no --> G["no block runs"]

The program below adds a third level to the hierarchy, moving_disc_t, and classifies four objects. The class is (disc_source_t) guard is written first on purpose: it still does not capture the moving_disc_t object, because a type is guard for the exact type wins. It also shows how to modify an object through the associating entity of a type is block, how to use a selector that is an expression (which needs an associate name), and how a polymorphic array is a valid selector.

module moving_sources
  use heatflow_sources, only: dp, heat_source_t, disc_source_t
  implicit none
  private
  public :: moving_disc_t

  ! A third level of the hierarchy: heat_source_t <- disc_source_t <- moving_disc_t.
  type, extends(disc_source_t) :: moving_disc_t
    real(dp) :: vx = 0.0_dp
    real(dp) :: vy = 0.0_dp
  end type moving_disc_t
end module moving_sources

program select_type_demo
  use heatflow_sources
  use moving_sources
  implicit none
  class(heat_source_t), allocatable :: s(:)      ! a polymorphic array: all elements share one dynamic type
  class(heat_source_t), allocatable :: one
  integer :: i

  allocate (one, source=moving_disc_t(disc_source_t(heat_source_t(0.5_dp, 0.5_dp, 9.0_dp), 0.1_dp), 0.2_dp, 0.0_dp))
  call classify(one)
  call classify(disc_source_t(heat_source_t(0.0_dp, 0.0_dp, 1.0_dp), 0.3_dp))
  call classify(line_source_t(heat_source_t(0.0_dp, 0.0_dp, 1.0_dp), 2.0_dp, 0.01_dp))
  call classify(heat_source_t(0.0_dp, 0.0_dp, 1.0_dp))

  ! Changing the object through the associating entity of a TYPE IS block.
  select type (one)
  type is (moving_disc_t)
    one%vx = one%vx + 0.5_dp
    print '(a, f4.1, a, f4.2)', 'moving disc: vx now ', one%vx, ', radius ', one%radius
  end select

  ! A selector that is an expression needs an associate name.
  select type (t => copy_of(one))
  class is (disc_source_t)
    print '(a, f8.5)', 'class is disc_source_t for copy_of(one), footprint ', t%footprint()
  end select

  allocate (s(2), source=disc_source_t(heat_source_t(0.0_dp, 0.0_dp, 1.0_dp), 0.5_dp))
  select type (s)
  type is (disc_source_t)
    do i = 1, size(s)
      s(i)%power = real(i, dp) * 10.0_dp
    end do
    print '(a, 2f6.1)', 'powers of the disc array after the loop:', s%power
  end select

contains

  function copy_of(src) result(c)
    class(heat_source_t), intent(in) :: src
    class(heat_source_t), allocatable :: c
    allocate (c, source=src)
  end function copy_of

  subroutine classify(src)
    class(heat_source_t), intent(in) :: src
    select type (src)
    class is (disc_source_t)                      ! listed first, but TYPE IS and the most specific CLASS IS win
      print '(a, f5.2)', 'class is disc_source_t (or an extension), radius ', src%radius
    type is (moving_disc_t)
      print '(a, f4.1, a, f4.1)', 'type is moving_disc_t, velocity ', src%vx, ' ', src%vy
    type is (line_source_t)
      print '(a, f5.2)', 'type is line_source_t, length ', src%length
    class default
      print '(a)', 'class default: some other heat source'
    end select
  end subroutine classify

end program select_type_demo

Output:

type is moving_disc_t, velocity  0.2  0.0
class is disc_source_t (or an extension), radius  0.30
type is line_source_t, length  2.00
class default: some other heat source
moving disc: vx now  0.7, radius 0.10
class is disc_source_t for copy_of(one), footprint  0.03142
powers of the disc array after the loop:  10.0  20.0

Note that classify(disc_source_t(...)) passed a structure constructor, which is not polymorphic, to a polymorphic dummy argument: its dynamic type is simply its declared type. A select type on the dummy of classify could not be written for a type(disc_source_t) dummy (see the errors below), because there is nothing to select.

The constraints are checked at compile time. The module below is deliberately non-conforming: a nonpolymorphic selector, a guard that is not an extension of the selector’s declared type, a duplicated guard and a duplicated class default.

! INTENTIONALLY NON-CONFORMING: constraints on SELECT TYPE (C1164, C1167, C1168, C1169).
module bad_select_type
  use heatflow_sources
  implicit none
contains
  subroutine not_polymorphic(d)
    type(disc_source_t), intent(in) :: d
    select type (d)                                   ! error: selector is not polymorphic
    type is (disc_source_t)
      print *, d%radius
    end select
  end subroutine not_polymorphic

  subroutine not_an_extension(s)
    class(disc_source_t), intent(in) :: s
    select type (s)
    type is (line_source_t)                           ! error: not an extension of disc_source_t
      print *, s%length
    end select
  end subroutine not_an_extension

  subroutine duplicates(s)
    class(heat_source_t), intent(in) :: s
    select type (s)
    type is (disc_source_t)
      print *, 'first'
    type is (disc_source_t)                           ! error: same type specified twice
      print *, 'second'
    class default
      print *, 'a'
    class default                                     ! error: at most one class default
      print *, 'b'
    end select
  end subroutine duplicates
end module bad_select_type

Diagnostics from GNU Fortran 16.2. The compiler reports the three errors in a different order than the source and does not report the duplicated class default at all in this file:

08_select_type_errors.f90:27:13:

   25 |     type is (disc_source_t)
      |             2
   26 |       print *, 'first'
   27 |     type is (disc_source_t)                           ! error: same type specified twice
      |             1
Error: TYPE IS at (1) overlaps with TYPE IS at (2)
08_select_type_errors.f90:17:13:

   17 |     type is (line_source_t)                           ! error: not an extension of disc_source_t
      |             1
Error: Derived type 'line_source_t' at (1) must be an extension of 'disc_source_t'
08_select_type_errors.f90:8:90:

    8 |     select type (d)                                   ! error: selector is not polymorphic
      |                                                                                          1
Error: Selector shall be polymorphic in SELECT TYPE statement at (1)

Diagnostics from LLVM Flang 23, which reports all four:

error: Semantic errors in 08_select_type_errors.f90
08_select_type_errors.f90:8:18: error: Selector 'd' in SELECT TYPE statement must be polymorphic
      select type (d)                                   ! error: selector is not polymorphic
                   ^
08_select_type_errors.f90:17:14: error: Type specification 'line_source_t' must be an extension of TYPE 'disc_source_t'
      type is (line_source_t)                           ! error: not an extension of disc_source_t
               ^^^^^^^^^^^^^
08_select_type_errors.f90:27:5: error: Type specification 'disc_source_t' conflicts with previous type specification
      type is (disc_source_t)                           ! error: same type specified twice
      ^^^^^^^^^^^^^^^^^^^^^^^
08_select_type_errors.f90:25:5: Conflicting type specification 'disc_source_t'
      type is (disc_source_t)
      ^^^^^^^^^^^^^^^^^^^^^^^
08_select_type_errors.f90:31:5: error: Type specification 'DEFAULT' conflicts with previous type specification
      class default                                     ! error: at most one class default
      ^^^^^^^^^^^^^
08_select_type_errors.f90:29:5: Conflicting type specification 'DEFAULT'
      class default
      ^^^^^^^^^^^^^

A select type chain is the object-oriented equivalent of a switch on the type. It is the right tool when the code needs the extension’s data (here, radius or vx) once, at the boundary of a subsystem; a chain that appears in many places is a sign that the behaviour should be a type-bound procedure of the hierarchy instead, so that a new type does not need edits everywhere. Control Flow covers the other selection constructs.

The associate construct

associate gives a name to an expression or a variable for the duration of a block (11.1.3). It is not specific to polymorphism, but it interacts with it in useful ways: the associating entity assumes the declared type of its selector, and it is polymorphic if and only if the selector is polymorphic, so a call through it still dispatches on the dynamic type. If the selector is a variable the name is an alias and can be assigned to; if it is an expression the associating entity is not definable. The expression is evaluated once, when the construct starts, and the associating entity keeps that value even if the variables the expression used change in the block (11.1.3.2).

program associate_demo
  use heatflow_sources
  implicit none
  class(heat_source_t), allocatable :: s
  type(heat_source_t) :: sources(2)

  allocate (s, source=disc_source_t(heat_source_t(0.5_dp, 0.25_dp, 6.0_dp), 0.1_dp))
  sources = [heat_source_t(0.0_dp, 0.0_dp, 1.0_dp), heat_source_t(1.0_dp, 1.0_dp, 3.0_dp)]

  ! A short name for a long designator; the associating entity is an alias, so assigning to it changes the target.
  associate (first => sources(1), total => sum(sources%power))
    first%power = first%power + 0.5_dp              ! allowed: first is associated with a variable
    print '(a, f5.2, a, f5.2)', 'first%power ', first%power, '   total of the original powers ', total
    ! total = 0.0_dp would be an error: total is associated with an expression, not a variable
  end associate

  ! The associating entity of a polymorphic selector is polymorphic and keeps the dynamic type, so dispatch still works.
  associate (src => s)
    call src%describe()
    print '(a, l1)', 'src is still a disc_source_t: ', same_type_as(src, disc_source_t())
    ! print *, src%radius   would not compile: the declared type of src is heat_source_t
  end associate

  ! The same, with the dynamic type exposed by SELECT TYPE and its own associate name.
  select type (disc => s)
  type is (disc_source_t)
    disc%radius = 0.2_dp
  end select
  call s%describe()
end program associate_demo

Output:

first%power  1.50   total of the original powers  4.00
  source at  0.50  0.25  power   6.00 W  footprint 0.03142
src is still a disc_source_t: T
  source at  0.50  0.25  power   6.00 W  footprint 0.12566

The total printed inside the construct, 4.00, is the sum evaluated when the construct was entered; the assignment to first%power did not change it. A target selector gives the associating entity the target attribute, a contiguous selector a contiguous associating entity, and an optional selector must be present on entry (11.1.3.3). A select type construct has the same association rules, and in the second part of the program select type (disc => s) re-types the same polymorphic variable and writes through it: the footprint printed afterwards is that of the new radius.

Unlimited polymorphism: class(*)

An entity declared class(*) is unlimited polymorphic (7.3.2.3). It is not declared to have a type, so it is not considered to have the same declared type as any other entity, and it is type compatible with every entity, including those of intrinsic type. It can be a dummy argument, an allocatable or a pointer, like any polymorphic entity, and it can be a component. The only things that can be done with it are to pass it on, to allocate it, to ask about its dynamic type with same_type_as and extends_type_of, and to select type on it, where type is guards may name intrinsic types and class is guards derived types.

The module below keeps heatflow settings of mixed types: each setting_t holds a key and a value of any type. set_setting allocates the component with source= the unlimited polymorphic dummy argument, which copies the dynamic type and the value, and describe_setting finds out what it holds with select type. The type is (character(len=*)) guard matches a character value of default kind and any length. The kinds matter: real(real64) and default real are different types and need their own guards.

module heatflow_settings
  use, intrinsic :: iso_fortran_env, only: real64, int32
  implicit none
  private
  public :: setting_t, set_setting, describe_setting

  type :: setting_t
    character(len=:), allocatable :: key
    class(*), allocatable :: value                 ! unlimited polymorphic: any type at all
  end type setting_t

contains

  subroutine set_setting(s, key, value)
    type(setting_t), intent(out) :: s
    character(len=*), intent(in) :: key
    class(*), intent(in) :: value
    s%key = key
    allocate (s%value, source=value)               ! copies the dynamic type and the value
  end subroutine set_setting

  subroutine describe_setting(s)
    type(setting_t), intent(in) :: s
    character(len=:), allocatable :: text
    character(len=32) :: buffer
    if (.not. allocated(s%value)) then
      print '(a, a)', s%key, ' = <unset>'
      return
    end if
    select type (v => s%value)
    type is (integer(int32))
      write (buffer, '(i0)') v
      text = 'integer(int32) ' // trim(buffer)
    type is (real(real64))
      write (buffer, '(es10.3)') v
      text = 'real(real64) ' // trim(adjustl(buffer))
    type is (real)
      write (buffer, '(es10.3)') v
      text = 'default real ' // trim(adjustl(buffer))
    type is (logical)
      if (v) then
        text = 'logical true'
      else
        text = 'logical false'
      end if
    type is (character(len=*))                   ! assumed length: matches any character length of default kind
      text = 'character(len=' // trim(str(len(v))) // ') "' // v // '"'
    class default
      text = 'something else'
    end select
    print '(a, a, a)', s%key, ' = ', text
  end subroutine describe_setting

  function str(n) result(r)
    integer, intent(in) :: n
    character(len=12) :: r
    write (r, '(i0)') n
  end function str

end module heatflow_settings

program unlimited_demo
  use, intrinsic :: iso_fortran_env, only: real64, int32
  use heatflow_settings
  implicit none
  type(setting_t) :: settings(6)
  integer :: i

  call set_setting(settings(1), 'nx', 64_int32)
  call set_setting(settings(2), 'alpha', 1.0e-4_real64)
  call set_setting(settings(3), 'tolerance', 1.0e-5)
  call set_setting(settings(4), 'periodic', .true.)
  call set_setting(settings(5), 'title', 'plate with hot spot')
  call set_setting(settings(6), 'origin', (1.0_real64, 2.0_real64))
  do i = 1, size(settings)
    call describe_setting(settings(i))
  end do

  ! An unallocated unlimited polymorphic component.
  settings(1) = setting_t('empty')
  call describe_setting(settings(1))
end program unlimited_demo

Output:

nx = integer(int32) 64
alpha = real(real64) 1.000E-04
tolerance = default real 1.000E-05
periodic = logical true
title = character(len=19) "plate with hot spot"
origin = something else
empty = <unset>

The complex value fell through to class default: there is no guard for complex, and an unlimited polymorphic value is never converted. An unallocated value component is legitimate and prints as unset. Because every guard is a separate branch, a function like this has to list every type it can meet; for a closed set of types, a derived type with a kind parameter (see Parameterized Derived Types) or a generic interface (see Generic and Rank Independent Programming) is usually the better tool, and class(*) is the tool for containers, callbacks with user data and similar open-ended cases.

The next program is deliberately non-conforming. It shows that an unlimited polymorphic object cannot be printed, converted or used in arithmetic outside a select type.

! INTENTIONALLY NON-CONFORMING: what an unlimited polymorphic object cannot do outside SELECT TYPE.
program bad_unlimited
  implicit none
  class(*), allocatable :: x
  integer :: n
  allocate (x, source=42)
  print *, x                         ! error: polymorphic list item
  n = x                              ! error: no conversion from class(*)
  n = x + 1                          ! error: no arithmetic on class(*)
end program bad_unlimited

Diagnostics from GNU Fortran 16.2, then LLVM Flang 23:

11_unlimited_errors.f90:7:37:

    7 |   print *, x                         ! error: polymorphic list item
      |                                     1
Error: Data transfer element at (1) cannot be polymorphic unless it is processed by a defined input/output procedure
11_unlimited_errors.f90:8:6:

    8 |   n = x                              ! error: no conversion from class(*)
      |      1
Error: Cannot convert CLASS(*) to INTEGER(4) at (1)
11_unlimited_errors.f90:9:6:

    9 |   n = x + 1                          ! error: no arithmetic on class(*)
      |      1
Error: Operands of binary numeric operator '+' at (1) are CLASS(*)/INTEGER(4)
error: Semantic errors in 11_unlimited_errors.f90
11_unlimited_errors.f90:7:12: error: I/O list item may not be unlimited polymorphic
    print *, x                         ! error: polymorphic list item
             ^
11_unlimited_errors.f90:8:3: error: No intrinsic or user-defined ASSIGNMENT(=) matches operand types INTEGER(4) and CLASS(*)
    n = x                              ! error: no conversion from class(*)
    ^^^^^
11_unlimited_errors.f90:9:7: error: Operands of + must be numeric; have CLASS(*) and INTEGER(4)
    n = x + 1                          ! error: no arithmetic on class(*)
        ^^^^^

The standard text behind the first error says a polymorphic list item is permitted only when a defined input/output procedure processes it (see Derived-Type and Asynchronous I/O).

class(*) versus type(*)

type(*) declares an assumed-type dummy argument (7.3.2.2). The standard also calls it unlimited polymorphic, but its use is restricted to what a C void * parameter allows: it cannot be allocatable, a pointer or have intent(out), it cannot appear in a designator or expression, and it can be passed on to another assumed-type dummy argument or to a few inquiry functions (C714 and C715). It exists for interoperability and for the MPI interfaces, covered in C Descriptors and ISO_Fortran_binding and MPI with mpi_f08. For Fortran-to-Fortran code use class(*).

Heterogeneous containers

An array declared class(heat_source_t), allocatable :: row(:) has one dynamic type for all its elements: it is allocated as a whole, and an element cannot be allocated on its own. A container of mixed types needs one polymorphic object per element, which means one level of indirection: a derived type whose only component is an allocatable polymorphic object (the slot idiom) and an array of those. The heterogeneous list below grows by doubling, moving the objects with move_alloc instead of copying them, and reports totals and the number of each type; it is the object-oriented heart of many Fortran applications.

module heatflow_source_list
  use heatflow_sources
  implicit none
  private
  public :: source_list_t

  ! A slot wraps one polymorphic object: an array of class(heat_source_t) cannot mix dynamic types,
  ! but an array of slots can, because each slot holds its own allocatable polymorphic component.
  type :: slot_t
    class(heat_source_t), allocatable :: item
  end type slot_t

  type :: source_list_t
    type(slot_t), allocatable :: slots(:)
    integer :: count = 0
  contains
    procedure :: add => list_add
    procedure :: total_power => list_total_power
    procedure :: total_footprint => list_total_footprint
    procedure :: report => list_report
  end type source_list_t

contains

  subroutine list_add(self, src)
    class(source_list_t), intent(inout) :: self
    class(heat_source_t), intent(in) :: src
    type(slot_t), allocatable :: bigger(:)
    integer :: i

    if (.not. allocated(self%slots)) allocate (self%slots(2))
    if (self%count == size(self%slots)) then
      allocate (bigger(2 * size(self%slots)))
      do i = 1, self%count
        call move_alloc(self%slots(i)%item, bigger(i)%item)   ! moves the object, no copy of the dynamic type
      end do
      call move_alloc(bigger, self%slots)
    end if
    self%count = self%count + 1
    allocate (self%slots(self%count)%item, source=src)
  end subroutine list_add

  real(dp) function list_total_power(self) result(total)
    class(source_list_t), intent(in) :: self
    integer :: i
    total = 0.0_dp
    do i = 1, self%count
      total = total + self%slots(i)%item%power
    end do
  end function list_total_power

  real(dp) function list_total_footprint(self) result(total)
    class(source_list_t), intent(in) :: self
    integer :: i
    total = 0.0_dp
    do i = 1, self%count
      total = total + self%slots(i)%item%footprint()          ! dynamic dispatch, one call per element
    end do
  end function list_total_footprint

  subroutine list_report(self)
    class(source_list_t), intent(in) :: self
    integer :: i, n_disc, n_line, n_other
    n_disc = 0
    n_line = 0
    n_other = 0
    do i = 1, self%count
      select type (s => self%slots(i)%item)
      type is (disc_source_t)
        n_disc = n_disc + 1
      type is (line_source_t)
        n_line = n_line + 1
      class default
        n_other = n_other + 1
      end select
    end do
    print '(a, i0, a, i0, a, i0, a, i0, a, i0, a)', 'count ', self%count, ' (capacity ', size(self%slots), '): ', &
      n_disc, ' discs, ', n_line, ' lines, ', n_other, ' other'
  end subroutine list_report

end module heatflow_source_list

program heterogeneous_list
  use heatflow_sources
  use heatflow_source_list
  implicit none
  type(source_list_t) :: list, copy
  integer :: i

  call list%add(heat_source_t(0.1_dp, 0.1_dp, 1.0_dp))
  call list%add(disc_source_t(heat_source_t(0.5_dp, 0.5_dp, 10.0_dp), 0.1_dp))
  call list%add(line_source_t(heat_source_t(0.2_dp, 0.8_dp, 4.0_dp), 0.5_dp, 0.02_dp))
  call list%add(disc_source_t(heat_source_t(0.7_dp, 0.3_dp, 6.0_dp), 0.05_dp))   ! forces the list to grow
  call list%report()
  print '(a, f6.2, a, f8.5)', 'total power', list%total_power(), '   total footprint', list%total_footprint()

  ! Intrinsic assignment of a derived type with allocatable components is a deep copy,
  ! including the dynamic type of every polymorphic component.
  copy = list
  select type (s => copy%slots(2)%item)
  type is (disc_source_t)
    s%radius = 1.0_dp
  end select
  print '(a, f8.5, a, f8.5)', 'footprint of the copy', copy%total_footprint(), '   of the original', list%total_footprint()
  do i = 1, copy%count
    call copy%slots(i)%item%describe()
  end do
end program heterogeneous_list

Output:

count 4 (capacity 4): 2 discs, 1 lines, 1 other
total power 21.00   total footprint 0.04927
footprint of the copy 3.15945   of the original 0.04927
  source at  0.10  0.10  power   1.00 W  footprint 0.00000
  source at  0.50  0.50  power  10.00 W  footprint 3.14159
  source at  0.20  0.80  power   4.00 W  footprint 0.01000
  source at  0.70  0.30  power   6.00 W  footprint 0.00785

The points that make this program correct:

  • move_alloc(from, to) moves the allocation: afterwards to has the dynamic type, bounds and value that from had, and from is unallocated. For polymorphic arguments to must be polymorphic if from is, and type compatible with it (16.9.147), so the slots' item components, which have the same declared type, qualify. Nothing is copied, no matter how large the objects are.

  • list_add receives a class(heat_source_t) and clones it with source=, so the list owns its objects whichever dynamic type the caller passes.

  • list_total_footprint makes one dynamically dispatched call per element, and list_report classifies elements by select type once, outside the arithmetic.

  • the last lines of the program assign copy = list. The rule quoted in Deep copies of polymorphic components allocates every slot of copy with the dynamic type of the corresponding slot of list, so changing the radius of an element of copy left list unchanged: the total footprint of the copy is 3.15945 and of the original 0.04927.

A heterogeneous list with a common declared type such as this one can use a different polymorphic design: a list that is a class itself, with the next-element pointer or allocatable component of type class(node_t), is a linked structure, covered with pointers in Pointers and Targets. The slot array is usually simpler and faster, because the elements are contiguous.

Polymorphic arrays

A polymorphic array has one dynamic type for all its elements, set by the type specification or by source= in the allocate statement. A class(t) assumed-shape dummy accepts an array of t, of any extension, polymorphic or not, and sections of these arrays stay polymorphic.

program polymorphic_arrays
  use heatflow_sources
  implicit none
  class(heat_source_t), allocatable :: row(:)
  type(disc_source_t) :: discs(3)

  ! One dynamic type for the whole array, set by the type-spec or the source expression.
  allocate (disc_source_t :: row(3))
  discs = [disc_source_t(heat_source_t(0.1_dp, 0.5_dp, 1.0_dp), 0.1_dp), &
           disc_source_t(heat_source_t(0.5_dp, 0.5_dp, 2.0_dp), 0.2_dp), &
           disc_source_t(heat_source_t(0.9_dp, 0.5_dp, 3.0_dp), 0.3_dp)]

  ! A nonpolymorphic array of the extension is accepted by a class(parent) dummy array ...
  print '(a, f8.5)', 'footprint of the type(disc_source_t) array: ', total_footprint(discs)
  ! ... and so is the polymorphic one.
  print '(a, f8.5)', 'footprint of the class array (defaults): ', total_footprint(row)
  ! Sections stay polymorphic.
  print '(a, f8.5)', 'footprint of row(2:3):                    ', total_footprint(row(2:3))

  deallocate (row)
  allocate (row(2), source=discs(2:3))              ! source= copies elements, so row takes both values and type
  print '(a, f8.5)', 'footprint of row after source=discs(2:3): ', total_footprint(row)

contains

  real(dp) function total_footprint(a) result(t)
    class(heat_source_t), intent(in) :: a(:)
    integer :: i
    t = 0.0_dp
    do i = 1, size(a)
      t = t + a(i)%footprint()
    end do
  end function total_footprint

end program polymorphic_arrays

Output:

footprint of the type(disc_source_t) array:  0.43982
footprint of the class array (defaults):  0.09425
footprint of row(2:3):                     0.06283
footprint of row after source=discs(2:3):  0.40841

The footprint of the class array allocated by type only (0.09425) is three discs with the default radius 0.1, and the last line shows source=discs(2:3) giving the new array both the type and the values of the section. A type(...) array of an extension, on the other hand, is not accepted by a type(parent) dummy array, and elements of a polymorphic array cannot be assigned by intrinsic assignment. The module below is deliberately non-conforming:

! INTENTIONALLY NON-CONFORMING: arrays of an extension and nonpolymorphic parent dummies.
module bad_arrays
  use heatflow_sources
  implicit none
contains
  subroutine take_parent_array(a)
    type(heat_source_t), intent(in) :: a(:)
    print *, size(a)
  end subroutine take_parent_array

  subroutine caller(discs)
    type(disc_source_t), intent(in) :: discs(:)
    call take_parent_array(discs)                       ! error: no implicit conversion of an array of extensions
  end subroutine caller

  subroutine element_assign(row, d)
    class(heat_source_t), intent(inout) :: row(:)
    type(disc_source_t), intent(in) :: d
    row(1) = d                                          ! error: element of a polymorphic dummy is not allocatable
  end subroutine element_assign
end module bad_arrays

Diagnostics from GNU Fortran 16.2, then LLVM Flang 23:

14_array_errors.f90:19:5:

   19 |     row(1) = d                                          ! error: element of a polymorphic dummy is not allocatable
      |     1~~~~~~
Error: Nonallocatable variable must not be polymorphic in intrinsic assignment at (1) - check that there is a matching specific subroutine for '=' operator
14_array_errors.f90:13:113:

   13 |     call take_parent_array(discs)                       ! error: no implicit conversion of an array of extensions
      |                                                                                                                 1
Error: Type mismatch in argument 'a' at (1); passed TYPE(disc_source_t) to TYPE(heat_source_t)
error: Semantic errors in 14_array_errors.f90
14_array_errors.f90:13:28: error: Actual argument type 'disc_source_t' is not compatible with dummy argument type 'heat_source_t'
      call take_parent_array(discs)                       ! error: no implicit conversion of an array of extensions
                             ^^^^^
14_array_errors.f90:19:5: error: Left-hand side of intrinsic assignment may not be polymorphic unless assignment is to an entire allocatable
      row(1) = d                                          ! error: element of a polymorphic dummy is not allocatable
      ^^^^^^

The arrays are the topic of Arrays Fundamentals and Array Arguments: Assumed Shape, Size and Rank; what changes with polymorphism is that elements have no individual identity to replace. To replace one element of a polymorphic array, use a slot container, or select type and assign the components.

The heatflow solver hierarchy

The real hierarchy of heatflow follows the same pattern at a larger scale. The abstract type solver_t owns the grid, the boundary condition and the step counter, declares the deferred binding step, and implements the loop run, which does not know which algorithm it is driving. jacobi_solver_t and ftcs_solver_t extend it with their own work arrays and implement step. The factory make_solver allocates the right extension from a string and returns it as a class(solver_t) allocatable, which is the only type the application has to name.

classDiagram class solver_t { <<abstract>> grid_t grid integer bc real boundary_value integer steps step(residual)* run(max_steps, tol, residual) } class jacobi_solver_t { real work step(residual) final jacobi_finalize } class ftcs_solver_t { real alpha real dt real work step(residual) } solver_t <|-- jacobi_solver_t solver_t <|-- ftcs_solver_t solver_t *-- grid_t : grid

Every procedure of the hierarchy is in the heatflow_solver module and its submodules (see Modules and Submodules); the interfaces and the bodies of the two step implementations live in different files, so that editing an algorithm does not recompile the application. The program below uses the real source files of the project. It builds a 21 by 21 plate with a hot spot in the middle, asks the factory for each of the two solvers, runs 200 steps through the inherited run and prints the result; a select type then shows the components that only one of the extensions has.

program solver_hierarchy
  use heatflow_kinds, only: dp, ip
  use heatflow_boundary, only: dirichlet
  use heatflow_grid, only: grid_t, make_grid
  use heatflow_solver, only: solver_t, jacobi_solver_t, ftcs_solver_t, make_solver
  implicit none
  type(grid_t) :: grid
  type(jacobi_solver_t) :: jacobi_mold
  class(solver_t), allocatable :: solver
  character(len=6), parameter :: kinds(2) = [character(len=6) :: 'jacobi', 'ftcs']
  real(dp) :: residual
  integer :: i

  grid = make_grid(21_ip, 21_ip, 1.0_dp, 1.0_dp)
  call grid%apply_boundary(dirichlet, 0.0_dp)
  call grid%set_hot_spot(100.0_dp)

  do i = 1, size(kinds)
    ! The factory returns class(solver_t); the caller never names the concrete type.
    solver = make_solver(kinds(i), grid, dirichlet, 0.0_dp, 1.0_dp, 1.0e-4_dp)
    print '(a, a, l1)', trim(kinds(i)), ': extends jacobi_solver_t? ', extends_type_of(solver, jacobi_mold)

    call solver%run(200_ip, 1.0e-12_dp, residual)         ! run is inherited; step is dispatched to the extension
    print '(a, i0, a, es10.3, a, f8.3)', '  steps ', solver%steps, '  residual', residual, &
      '  max temperature', solver%grid%max_temperature()

    select type (solver)
    type is (ftcs_solver_t)
      print '(a, es9.2, a, es9.2)', '  ftcs-only components: alpha', solver%alpha, '  dt', solver%dt
    type is (jacobi_solver_t)
      print '(a, l1)', '  jacobi-only component work allocated: ', allocated(solver%work)
    end select
  end do
end program solver_hierarchy

The sources of the eight heatflow_* modules are the ones of the project (heatflow_kinds to heatflow_ftcs); they are compiled in that order before the program.

Output:

jacobi: extends jacobi_solver_t? T
  steps 200  residual 1.679E-01  max temperature   0.168
  jacobi-only component work allocated: T
ftcs: extends jacobi_solver_t? F
  steps 200  residual 5.113E-03  max temperature   1.006
  ftcs-only components: alpha 1.00E+00  dt 1.00E-04

Three design decisions of that hierarchy are worth copying:

  • Dispatch happens once per sweep. run calls self%step(residual) once per iteration, and each step calls a pure array kernel (jacobi_sweep or ftcs_sweep in heatflow_kernels) that does the work of a whole grid sweep. The dynamic dispatch of the call is negligible against the sweep, and the kernel is a plain procedure on arrays that the compiler can optimize without knowing about the hierarchy.

  • The caller never names a concrete type. The application holds a class(solver_t), allocatable and uses run, steps and grid; only make_solver and the type guards in tests and diagnostics know jacobi_solver_t and ftcs_solver_t.

  • Data common to all variants is in the base type. The grid, the boundary condition and the counter are components of solver_t and are inherited by each extension, so the variants add only what is specific to them.

Inquiry and move intrinsics

Intrinsic Result Notes

same_type_as(a, b)

true if the dynamic types of a and b are the same extensible type

16.9.177; the processor dependent result for non-extensible types means it should only be used on extensible or unlimited polymorphic objects; an unallocated variable has its declared type

extends_type_of(a, mold)

true if the dynamic type of a is an extension of the dynamic type of mold (or equal)

16.9.86; ignores kind parameters; with an unlimited polymorphic mold that is unallocated the result is true

storage_size(a)

size in bits of an object of the dynamic type, as it would be stored in an array

16.9.200; Fortran 2008; for a polymorphic argument it reports the dynamic type, as the output of the first example showed

move_alloc(from, to)

moves the allocation of from to to and deallocates from

16.9.147; to must be polymorphic if from is; no copy of the data

allocated(x), associated(p)

allocation and association status

how to test whether a polymorphic variable has a dynamic type of its own

typeof and classof

Fortran 2023 added two declaration specifiers that copy the type of a previously declared entity (7.3.2.1, R703): typeof(x) declares an entity of the declared type and type parameters of x, nonpolymorphic, and classof(x) one that is polymorphic with the same declared type. They are useful in generic code and in the templates of Fortran 2028 because they track a declaration instead of repeating it. classof of a class(*) entity is class(*). The program below uses both; the two compilers disagree.

! F2023 TYPEOF and CLASSOF declaration specifiers.
program typeof_demo
  use heatflow_sources
  implicit none
  type(disc_source_t) :: d
  typeof(d) :: another
  class(heat_source_t), allocatable :: s
  classof(s), allocatable :: t
  another = d
  allocate (disc_source_t :: s)
  allocate (t, source=s)
  print *, same_type_as(another, d), same_type_as(s, t)
end program typeof_demo

Output with LLVM Flang 23:

 T T

Diagnostics from GNU Fortran 16.2, which does not know the specifiers: the first message is the real error, and the other three follow from it:

18_typeof.f90:6:7:

    6 |   typeof(d) :: another
      |       1
Error: Mangled derived type definition at (1)
18_typeof.f90:11:13:

   11 |   allocate (t, source=s)
      |             1
Error: Allocate-object at (1) is neither a data pointer nor an allocatable variable
18_typeof.f90:9:3:

    9 |   another = d
      |   1~~~~~~
Error: Symbol 'another' at (1) has no IMPLICIT type
18_typeof.f90:12:54:

   12 |   print *, same_type_as(another, d), same_type_as(s, t)
      |                                                      1
Error: Symbol 't' at (1) has no IMPLICIT type
18_typeof.f90:12:54:

   12 |   print *, same_type_as(another, d), same_type_as(s, t)
      |                                                      1
Error: Symbol 't' at (1) has no IMPLICIT type

The Flang table lists the specifiers as supported, which matches what the example showed. GNU Fortran has no Fortran 2023 status page, so the example is the only evidence for gfortran; keep the specifiers out of code that must build with it until a later version accepts them. The rules (C708 to C713), the use for the kind and rank axes of generic code, and the support of the other compilers are in Generic and Rank Independent Programming and History, Standards and Compilers.

Object-based or object-oriented

The Fortran features for objects fall in two sets, and most good Fortran uses the first one much more than the second.

Object-based programming uses derived types, modules, private components and type-bound procedures to bundle data with the operations on it. No class is involved, every call is resolved at compile time (the compiler knows the type of grid is grid_t), the compiler can inline across the binding, and the type may still be extended later. grid_t in heatflow, with apply_boundary, set_hot_spot and max_temperature, is an example. The fortran-lang tutorial on object-based techniques builds the same kind of module (a function type with parameters) and the one on object-oriented techniques adds extension, abstract types and polymorphism to it.

Object-oriented programming adds extension and class: a program that must pick the numerical method, the boundary treatment or the output format at run time, or must hold a list of different things and treat them uniformly, needs dynamic dispatch. solver_t is an example. The cost is real but small, and it is paid per call, so it matters when a procedure does little work and is called for each element of a big array.

What dispatch costs

The fortran-lang page on performance and ease of use measures a small function called through an object-oriented design and names the two overheads: the procedure call itself and the run-time type resolution; its advice is to give the framework an array-based version of the function, so that the overheads are paid once and the numerical kernel can be optimized by the compiler. heatflow follows that advice. To get a feeling for the numbers, the program below evaluates a material conductivity law 50 million times in four ways: through a type(...) variable (static binding), through a polymorphic variable with a single dynamic type, through a polymorphic variable whose dynamic type alternates at every call, and as an elemental function applied to arrays. It uses an abstract type with a deferred binding, which is the subject of the next page, and is compiled with -O2.

module heatflow_materials
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: dp, material_t, linear_material_t, saturating_material_t, slot_t, conductivity_elemental
  integer, parameter :: dp = real64

  type, abstract :: material_t
  contains
    procedure(conductivity_iface), deferred :: conductivity
  end type material_t

  abstract interface
    pure function conductivity_iface(self, t) result(k)
      import :: material_t, dp
      class(material_t), intent(in) :: self
      real(dp), intent(in) :: t
      real(dp) :: k
    end function conductivity_iface
  end interface

  type, extends(material_t) :: linear_material_t
    real(dp) :: k0 = 1.0_dp, slope = 0.01_dp
  contains
    procedure :: conductivity => linear_k
  end type linear_material_t

  type, extends(material_t) :: saturating_material_t
    real(dp) :: kmax = 2.0_dp, scale = 50.0_dp
  contains
    procedure :: conductivity => saturating_k
  end type saturating_material_t

  type :: slot_t
    class(material_t), allocatable :: m
  end type slot_t

contains

  pure function linear_k(self, t) result(k)
    class(linear_material_t), intent(in) :: self
    real(dp), intent(in) :: t
    real(dp) :: k
    k = self%k0 + self%slope * t
  end function linear_k

  pure function saturating_k(self, t) result(k)
    class(saturating_material_t), intent(in) :: self
    real(dp), intent(in) :: t
    real(dp) :: k
    k = self%kmax * t / (t + self%scale)
  end function saturating_k

  ! The object-based version of the linear law: a plain elemental function, no object at all.
  elemental function conductivity_elemental(k0, slope, t) result(k)
    real(dp), intent(in) :: k0, slope, t
    real(dp) :: k
    k = k0 + slope * t
  end function conductivity_elemental
end module heatflow_materials

program dispatch_cost
  use, intrinsic :: iso_fortran_env, only: int64
  use heatflow_materials
  implicit none
  integer, parameter :: nt = 1000, nrep = 50000          ! 50 million evaluations on 1000 cache-resident temperatures
  type(linear_material_t) :: lin
  class(material_t), allocatable :: dyn
  type(slot_t) :: slots(2)
  real(dp) :: t(nt), k(nt)
  real(dp) :: total
  integer(int64) :: c0, c1, rate
  integer :: i, rep

  t = [(real(mod(i, 100), dp), i=1, nt)]
  allocate (slots(1)%m, source=linear_material_t())
  allocate (slots(2)%m, source=saturating_material_t())
  allocate (dyn, source=linear_material_t())
  call system_clock(count_rate=rate)

  total = 0.0_dp
  call system_clock(c0)
  do rep = 1, nrep
    do i = 1, nt                                 ! static type: the compiler knows the binding
      total = total + lin%conductivity(t(i))
    end do
  end do
  call system_clock(c1)
  call report('type(linear_material_t)', c0, c1, rate, total)

  total = 0.0_dp
  call system_clock(c0)
  do rep = 1, nrep
    do i = 1, nt                                 ! class(...) with one dynamic type
      total = total + dyn%conductivity(t(i))
    end do
  end do
  call system_clock(c1)
  call report('class(material_t), one type', c0, c1, rate, total)

  total = 0.0_dp
  call system_clock(c0)
  do rep = 1, nrep
    do i = 1, nt                                 ! the dynamic type alternates, so the target changes every call
      total = total + slots(1 + iand(i, 1))%m%conductivity(t(i))
    end do
  end do
  call system_clock(c1)
  call report('class(material_t), two types', c0, c1, rate, total)

  total = 0.0_dp
  call system_clock(c0)
  do rep = 1, nrep
    k = conductivity_elemental(1.0_dp, 0.01_dp, t)   ! object-based: plain elemental function on arrays
    total = total + sum(k)
  end do
  call system_clock(c1)
  call report('elemental function', c0, c1, rate, total)

contains

  subroutine report(label, c0, c1, rate, total)
    character(len=*), intent(in) :: label
    integer(int64), intent(in) :: c0, c1, rate
    real(dp), intent(in) :: total
    print '(a30, a, f8.1, a, es11.4)', label, ' : ms ', 1.0e3_dp * real(c1 - c0, dp) / real(rate, dp), '  sum', total
  end subroutine report

end program dispatch_cost

Output of one run with gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -O2 on an aarch64 Linux container (the times in milliseconds are machine, load and compiler dependent; the sums are exact):

       type(linear_material_t) : ms     41.3  sum 7.4750E+07
   class(material_t), one type : ms     49.2  sum 7.4750E+07
  class(material_t), two types : ms     50.7  sum 5.9786E+07
            elemental function : ms     59.5  sum 7.4750E+07

Read the numbers as a demonstration of the structure and not as a benchmark: with the optimizer enabled GNU Fortran saw through the calls (in quiet runs the static, the single-type polymorphic and the two-type polymorphic loops were within about 30 percent of each other, and repeated runs on this machine varied by a few milliseconds, more when the machine was busy). LLVM Flang 23 with -O2 showed the polymorphic loops between 1.2 and 1.8 times slower than the static one in the runs made for this page. The elemental version was the slowest of the four in the gfortran runs, most likely because it makes a second pass over a temporary array; it can only win when the calls cannot be inlined. What the measurements do show is that the cost of dispatch is a small, constant amount per call, so the usual guidance applies:

  • dispatch at the coarse level (a solver step, a file format, a boundary condition) and keep the inner loops monomorphic and array-based;

  • if a loop must call through a polymorphic variable, hoist the select type outside the loop and run a monomorphic loop in each branch;

  • keep hierarchies shallow and the type-bound procedures small, because a call that cannot be inlined also blocks vectorization;

  • avoid polymorphic components in arrays of small objects (the slot idiom allocates one object per element), and prefer structure of arrays (separate arrays of x, y and power) for data that is processed in bulk; see Performance and Memory Layout.

Design guidance

  • Prefer composition to extension when there is no is-a relation. A solver_t has a grid_t; it is not one.

  • Make the base type small and stable. Everything in the base is inherited by every extension, and changes to it recompile everyone.

  • Make the public interface the bindings. Keep components private where the module boundary allows and let clients call bindings; extension in another module inherits the private components without being able to see them (see Private components and extension).

  • Write the passed-object dummy as class(t) in every binding, including those that do not need polymorphism: a binding that uses type(t) for its object is not allowed for an extensible type (C765).

  • Provide a clone idiom (a function with an allocatable polymorphic result and source=) when callers must copy objects of unknown dynamic type.

  • Use class(*) at the edges: containers, callbacks with user data and generic utilities, where the code that created the value is also the code that selects it again.

  • Keep select type in one place, such as a factory or a serializer. A switch on the type in many places is the maintenance problem that polymorphism is meant to remove.

  • Do not rely on finalization for correctness. GNU Fortran’s manual says its support for finalization is incomplete (see Type-Bound Procedures, Abstract Types and Finalization).

Where the books and older tutorials are outdated

The two books consulted for this section cover the object-oriented chapters at the concept level, and the standard’s own text is the reference. Where they differ from current practice:

Area Outdated in the books and older material, current approach

Compiler support

the books carry no compiler-support information for Fortran 2023 features; for this page, GNU Fortran 16.2 and LLVM Flang 23 build every example except typeof/classof, which only Flang 23 accepts, and GNU Fortran still describes its finalization support as incomplete

Fortran 2023 additions

declaration with typeof and classof is new in Fortran 2023 and absent from material written for Fortran 2018 and earlier

Fortran 95 teaching material

the fortran-lang pages on Fortran 95 features contain no object-oriented features at all; all of this page is Fortran 2003 or later

Genericity

older material uses class(*) and include files to emulate generic containers; the Fortran 2028 working draft adds templates (J3/26-007r1), which are not available in any compiler checked here, so class(*) and generic interfaces remain the tools

Build tools

multi-file hierarchies are built with module-aware tools (fpm, CMake, Meson) that order the compilation by use dependencies; hand-written make rules from older books are rarely needed

Standard version provenance and compiler support

Feature Introduced Compiler support checked on 2026-10-19

extends, parent component, class(t), class(*), select type, associate, same_type_as, extends_type_of, allocate with source=, move_alloc

Fortran 2003 (the GNU Fortran wiki lists them in its Fortran 2003 status)

GNU Fortran 16.2 and LLVM Flang 23 build the examples

allocate with mold=, polymorphic assignment (reallocation to the dynamic type), storage_size

Fortran 2008 (GNU Fortran wiki, Fortran 2008 status)

both

typeof, classof

Fortran 2023 (R703, WG5 N2212)

accepted by LLVM Flang 23, rejected by GNU Fortran 16.2

Templates and generic subprograms as the type-safe alternative to class(*)

Fortran 2028 working draft J3/26-007r1 only

not available in the compilers checked

Not checked here, because no compiler for them is installed: Intel ifx, NAG, HPE Cray and NVIDIA HPC compilers. See History, Standards and Compilers for the per-compiler support table and What’s New and Fortran 2028 Preview for the templates.

Pitfalls

  • A class variable is not a pointer. It is a value that can have any extension’s type; the extension’s data is in it, and copying it copies that data, unlike a Java reference.

  • Slicing is silent. Assigning or passing a polymorphic value to a type(parent) variable or dummy argument drops the extension and the overriding bindings: the dispatch starts from the declared type again.

  • class(parent), allocatable dummy arguments need class(parent) actual arguments. A class(child) variable is rejected.

  • A polymorphic dummy argument cannot be assigned. Intrinsic assignment needs an allocatable variable; give the type a defined assignment or make the dummy allocatable.

  • Every element of a polymorphic array has the same dynamic type. For a mixed list, wrap each object in a slot type.

  • An unallocated polymorphic variable has its declared type, so same_type_as can be true for an object that does not exist; and a call through it is undefined (GNU Fortran with -fcheck=all stops with a message, Flang 23 crashed in the example above).

  • mold= copies the type, not the value. Use source= when the value is needed.

  • The guards of select type are not tried in source order. The most particular matching guard wins, so a general class is placed first does not shadow a type is.

  • select type needs a polymorphic selector. A type(...) variable has no dynamic type to select on.

  • type is (real) does not match real(real64). Each kind is a different type; list every kind you accept.

  • An unlimited polymorphic object cannot be printed, converted or added. Go through select type.

  • Do not extend a type just to share code. If the relation is has-a, use a component; if it is a family of procedures, use a module.

  • typeof and classof are not available in GNU Fortran 16.2.

  • Finalization is the weakest area of GNU Fortran’s object-oriented support: its manual calls it incomplete. Test it on every compiler that you target.