Derived Types

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 every example was also built and run with LLVM Flang 23; the output shown is the real output of GNU Fortran unless it 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 clause 7 of the standard ("Types"): 7.5.1 gives the concepts, 7.5.2 the type definition, 7.5.4 the components, 7.5.9 the derived-type specifier, 7.5.10 the construction of values and 7.5.11 operations and assignment. Structure components (the percent selector) are in 9.4.2, intrinsic assignment of structures is in 10.2.1.3, and the interoperable form is in 18.3.4.

A derived type is how a Fortran program names a record: a collection of components, each with its own type, kind and attributes, that is treated as one value when it is assigned, passed to a procedure or returned from a function. Everything the running example, heatflow, stores about the plate is a derived type: grid_t holds the extents, the spacings and the allocatable array of temperatures, config_t holds the simulation parameters with defaults, and the solver hierarchy that comes later is built by extending derived types. This page covers the type itself and the values and objects made from it. The related features have their own pages: type parameters in Parameterized Derived Types, type extension and class in Object Oriented Programming, procedures bound to a type, abstract types and finalization in Type-Bound Procedures, Abstract Types and Finalization, and defined input and output in Derived-Type and Asynchronous I/O.

Defining a derived type

A type definition (R726) is a scoping construct made of a type statement, an optional parameter and private or sequence part, the components, an optional contains part for type-bound procedures, and end type:

type [[, attribute-list] ::] name [(type-parameter-names)]
  [private | sequence]
  component definitions
[contains
  type-bound procedure part]
end type [name]

The attributes allowed on the type statement (R728) are abstract, bind(c), extends(parent), and the accessibility attributes public and private. The double colon is optional only when there is no attribute list. If end type repeats the name, it must be the same name (C744), and the name must not be the name of an intrinsic type (C734), so the standard does not allow type :: integer or type :: real.

Components are declared much like variables (R737): a type specification, an optional attribute list and a list of names with optional array bounds and initialization. The next example defines the three kinds of structure most programs start with. point_t has two scalar components, probe_t has a character component and a derived-type component of a previously defined type, and plate_t has an explicit-shape array component and an allocatable array component. The main program then builds, reads and modifies values.

module heatflow_geometry
  use, intrinsic :: iso_fortran_env, only: real64, int32
  implicit none
  private
  public :: dp, point_t, probe_t, plate_t

  integer, parameter :: dp = real64

  type :: point_t                       ! two scalar components
    real(dp) :: x
    real(dp) :: y
  end type point_t

  type :: probe_t                       ! a derived-type component, a character component
    character(len=8) :: label
    type(point_t) :: position          ! nested structure
    integer(int32) :: samples
  end type probe_t

  type :: plate_t                       ! array and allocatable components
    real(dp) :: size(2)                ! explicit-shape array component: length and height
    integer(int32) :: cells(2)
    real(dp), allocatable :: temperature(:, :)
  end type plate_t
end module heatflow_geometry

program derived_type_basics
  use heatflow_geometry
  implicit none
  type(point_t) :: corner, centre
  type(probe_t) :: probe
  type(plate_t) :: plate
  type(point_t) :: points(3)

  corner = point_t(0.0_dp, 0.0_dp)                    ! positional constructor
  centre = point_t(y=0.5_dp, x=1.0_dp)                ! keyword constructor, any order
  probe = probe_t('centre', centre, 0)                ! a nested structure value
  probe%samples = probe%samples + 10                  ! component access with %
  probe%position%x = 2.0_dp                           ! component of a component

  plate = plate_t(size=[2.0_dp, 1.0_dp], cells=[5, 3], temperature=null())
  print '(a, l1)', 'temperature allocated before: ', allocated(plate%temperature)
  allocate (plate%temperature(plate%cells(1), plate%cells(2)), source=20.0_dp)
  plate%temperature(3, 2) = 100.0_dp
  print '(a, l1, a, f6.1)', 'allocated now: ', allocated(plate%temperature), '  max=', maxval(plate%temperature)

  points = [point_t(0.0_dp, 0.0_dp), point_t(1.0_dp, 0.0_dp), point_t(1.0_dp, 1.0_dp)]
  print '(a, 3f5.1)', 'x of every point: ', points%x    ! component of an array of structures is an array
  print '(a, 2f5.1)', 'y of points 2:3 : ', points(2:3)%y

  print *, 'probe label=', probe%label, ' x=', probe%position%x, ' samples=', probe%samples
  print *, 'corner as a list-directed item: ', corner
  print *, 'centre: ', centre
end program derived_type_basics

Output:

temperature allocated before: F
allocated now: T  max= 100.0
x of every point:   0.0  1.0  1.0
y of points 2:3 :   0.0  1.0
 probe label=centre   x=   2.0000000000000000       samples=          10
 corner as a list-directed item:    0.0000000000000000        0.0000000000000000
 centre:    1.0000000000000000       0.50000000000000000

A few points in that program are worth stating precisely.

  • The percent sign selects a component (R911). probe%position%x is a component of a component; a selector may be applied to an element of an array of structures (points(2)%y) and to a whole array of structures, in which case the result is an array (points%x). Only one part of a designator may have a nonzero rank (C919), so a(1:3)%b(1:2) is invalid, and a part to the right of a part with nonzero rank cannot be allocatable or a pointer.

  • A component can be anything a variable can be, with some restrictions (C749 to C759): a derived-type component that is neither a pointer nor allocatable must have a type that is already defined (see below); an array component with neither attribute must have explicit bounds that are specification expressions of the type’s parameters or constants, and with the allocatable or pointer attribute it must have deferred shape ((:), (:, :)); a component cannot be both allocatable and a pointer; contiguous applies only to pointer array components; and the *len form is allowed only for character components.

  • The values of a type are the sequences of values of its components (7.5.8). The value of a pointer component is its association, and the value of an allocatable component is its allocation status together with its dynamic type, bounds and value if allocated.

  • List-directed output of a structure lists its components in component order (12.6.3), which is the order of declaration (7.5.4.7). The spacing and the number of digits are compiler choices, which is why the two compilers print x= 2.0000000000000000 and x= 2. for the same value.

  • The definition must come before it is used in a data component. The next listing shows what the compilers say about a type(b_t) component of a type defined later. A pointer or allocatable component may name the type being defined or one defined later, which is what makes linked structures possible (see Recursive types). A type with no components at all is also allowed (the storage size of such a type in GNU Fortran and Flang is zero bits).

This is deliberately non-conforming and shows C749:

! INTENTIONALLY NON-CONFORMING: a derived-type component of a type that is defined later.
program order_and_empty
  implicit none

  type :: a_t
    type(b_t) :: inner                ! error: b_t is not defined yet and the component is neither pointer nor allocatable
  end type a_t

  type :: b_t
    integer :: n = 0
  end type b_t
end program order_and_empty

GNU Fortran 16.2:

18_order_and_empty.f90:6:17:

    6 |     type(b_t) :: inner                ! error: b_t is not defined yet and the component is neither pointer nor allocatable
      |                 1
Error: Derived type at (1) has not been previously defined and so cannot appear in a derived type definition

LLVM Flang 23:

error: Semantic errors in 18_order_and_empty.f90
18_order_and_empty.f90:6:10: error: Derived type 'b_t' not found
      type(b_t) :: inner                ! error: b_t is not defined yet and the component is neither pointer nor allocatable
           ^^^

Kinds of components

Component Declaration Remarks

Scalar of intrinsic type

real(dp) :: dx, character(len=16) :: solver

the kind and length are constant expressions or type parameters

Explicit-shape array

real(dp) :: lengths(2) or real(dp), dimension(2) :: lengths

bounds are constant or specification expressions; the shape is part of the type

Allocatable scalar or array

real(dp), allocatable :: u(:, :)

starts unallocated; owned by the object; copied by assignment

Pointer scalar or array

real(dp), pointer :: view(:) => null()

only the association is part of the value; see Pointers and Targets

Derived type

type(point_t) :: position

the type must be defined first

Recursive derived type

type(node_t), allocatable :: next or type(node_t), pointer :: next => null()

the only way to name the type being defined; see Recursive types

Procedure pointer

procedure(source_fn), pointer, nopass :: rate => null()

see Procedure pointer components

Coarray

real, allocatable, codimension[:] :: x

allocatable only; see Coarrays, Images and Synchronization

Character with length

character(len=:), allocatable :: label

deferred-length allocatable characters are allowed as components, see Characters and Strings

Type parameters (kind and len) are not components, are effectively public (7.5.4.8, Note 1) and are covered in Parameterized Derived Types. Constants, enumerators and parameter attributes are not allowed on components: a component is always a variable, and a constant belongs in the enclosing module.

Structure constructors

A derived-type definition implicitly defines a structure constructor with the same name as the type (R756). It builds a scalar value from component values:

type-name ( [component-value-list] )

Values may be given in component order (positional) or with the component name as a keyword, and the two may be mixed as long as the keyword form is last: a positional value cannot follow a keyword value (C7105). The rules, all checked by the compiler, are:

  • a value must be provided for every nonallocatable component that has no default initialization (C7104);

  • a component can be given at most once (C7102), and every keyword must be a component name (C7106);

  • the type name and every component for which a value is provided must be accessible where the constructor is written (C7107), which is what makes a type with private components impossible to construct outside its module unless the module offers its own constructor, see Custom constructors;

  • the type must not be abstract (C7101);

  • the value is converted to the component with the rules of intrinsic assignment: an integer value for a real component is converted, a character value is padded or truncated to the component length (GNU Fortran warns about the truncation with -Wcharacter-truncation), a scalar is broadcast to every element of an array component, and the shape of an array value must conform;

  • a constructor cannot appear before the type is defined.

The next program is deliberately non-conforming and breaks each rule in turn; the line after each statement says which.

! INTENTIONALLY NON-CONFORMING: invalid structure constructors.
program constructor_errors
  implicit none
  type :: window_t
    integer :: nx                      ! no default
    integer :: ny                      ! no default
    real :: scale = 1.0
  end type window_t
  type(window_t) :: w

  w = window_t(nx=10)                  ! error: ny has no default value
  w = window_t(10, 20, 2.0, 99)        ! error: too many component values
  w = window_t(10, 20, depth=3.0)      ! error: depth is not a component
  w = window_t(nx=10, 20, 2.0)         ! error: positional value after a keyword value
  w = window_t(10, 'twenty', 2.0)      ! error: a character value for an integer component
  w = window_t(10, 20, nx=1)           ! error: nx given twice
end program constructor_errors

GNU Fortran 16.2 reports the statements in this order, not in source order:

11_constructor_errors.f90:14:22:

   14 |   w = window_t(nx=10, 20, 2.0)         ! error: positional value after a keyword value
      |                      1
Error: Missing keyword name in actual argument list at (1)
11_constructor_errors.f90:11:7:

   11 |   w = window_t(nx=10)                  ! error: ny has no default value
      |       1
Error: No initializer for component 'ny' given in the structure constructor at (1)
11_constructor_errors.f90:12:28:

   12 |   w = window_t(10, 20, 2.0, 99)        ! error: too many component values
      |                            1
Error: Too many components in structure constructor at (1)
11_constructor_errors.f90:13:7:

   13 |   w = window_t(10, 20, depth=3.0)      ! error: depth is not a component
      |       1
Error: 'depth' at (1) is not a member of the 'window_t' structure
11_constructor_errors.f90:15:19:

   15 |   w = window_t(10, 'twenty', 2.0)      ! error: a character value for an integer component
      |                   1
Error: Cannot convert CHARACTER(6) to INTEGER(4) at (1)
11_constructor_errors.f90:16:26:

   16 |   w = window_t(10, 20, nx=1)           ! error: nx given twice
      |                          1
Error: Component 'nx' is initialized twice in the structure constructor at (1)

LLVM Flang 23:

error: Semantic errors in 11_constructor_errors.f90
11_constructor_errors.f90:11:7: error: Structure constructor lacks a value for component 'ny'
    w = window_t(nx=10)                  ! error: ny has no default value
        ^^^^^^^^
11_constructor_errors.f90:6:16: Declaration of 'ny'
      integer :: ny                      ! no default
                 ^^
11_constructor_errors.f90:12:29: error: Unexpected value in structure constructor
    w = window_t(10, 20, 2.0, 99)        ! error: too many component values
                              ^^
11_constructor_errors.f90:13:24: error: Keyword 'depth=' does not name a component of derived type 'window_t'
    w = window_t(10, 20, depth=3.0)      ! error: depth is not a component
                         ^^^^^
11_constructor_errors.f90:14:23: error: Value in structure constructor lacks a component name
    w = window_t(nx=10, 20, 2.0)         ! error: positional value after a keyword value
                        ^^
11_constructor_errors.f90:14:27: error: Value in structure constructor lacks a component name
    w = window_t(nx=10, 20, 2.0)         ! error: positional value after a keyword value
                            ^^^
11_constructor_errors.f90:15:20: error: Value in structure constructor of type 'CHARACTER(KIND=1,LEN=6_8)' is incompatible with component 'ny' of type 'INTEGER(4)'
    w = window_t(10, 'twenty', 2.0)      ! error: a character value for an integer component
                     ^^^^^^^^
11_constructor_errors.f90:6:16: Declaration of 'ny'
      integer :: ny                      ! no default
                 ^^
11_constructor_errors.f90:16:24: error: Component 'nx' conflicts with another component earlier in this structure constructor
    w = window_t(10, 20, nx=1)           ! error: nx given twice
                         ^^

The constructor treats the three kinds of data component differently, as the standard describes in 7.5.10:

  • A nonpointer, nonallocatable component receives the converted value. Its shape must conform.

  • A pointer component is given a data-target or proc-target and becomes pointer-associated with it (or takes over the association of a pointer value, or becomes disassociated for null()). A constructor that gives a variable to a pointer component therefore leaves a pointer to that variable in the structure, and the variable needs the target attribute.

  • An allocatable component may be given an expression, which is allocated with the shape and value of the expression; an allocatable entity, whose allocation status and value are copied; or the function null() with no arguments, which leaves the component unallocated. Since Fortran 2008 the component may be left out altogether, and it is then unallocated. Both compilers accept the omission, as the following program shows.

program omit_allocatable
  implicit none
  type :: field_t
    integer :: n
    real, allocatable :: u(:)
  end type field_t
  type(field_t) :: f
  f = field_t(n=4)                          ! the allocatable component is omitted
  print '(a, i0, a, l1)', 'n=', f%n, '  u allocated: ', allocated(f%u)
  f = field_t(3, [1.0, 2.0, 3.0])           ! a non-allocatable value gives an allocated component
  print '(a, i0, a, l1, a, i0)', 'n=', f%n, '  u allocated: ', allocated(f%u), '  size=', size(f%u)
end program omit_allocatable

Output:

n=4  u allocated: F
n=3  u allocated: T  size=3

A constructor with constant expressions as values is itself a constant expression, so it can initialize a named constant or a variable, or give a default to a component of another type:

type(run_config_t), parameter :: reference = run_config_t(steps=0)
type(point_t), save :: origin = point_t(0.0_dp, 0.0_dp)

A constructor can also appear inside an array constructor, to build an array of structures: [point_t(0.0_dp, 0.0_dp), point_t(1.0_dp, 0.0_dp)]. All the elements must have the same type, and, for a parameterized type, the same type parameters.

Default initialization

A component can be given a default initialization (R743): = constant-expr for a nonpointer, nonallocatable component, => null() or => a target for a pointer component. Every object of the type, whether a variable, a function result, a dummy argument with intent(out) or an allocated object, starts with those values. A component without an initializer is undefined, except that allocatable components are always unallocated at the start (7.5.4.6). A double colon is required in the component declaration when an initializer is present (C766).

module heatflow_defaults
  use, intrinsic :: iso_fortran_env, only: real64, int32
  implicit none
  private
  public :: dp, run_config_t, node_t, defaults_demo

  integer, parameter :: dp = real64

  type :: run_config_t
    integer(int32) :: nx = 41_int32                 ! default initialization
    integer(int32) :: ny = 41_int32
    real(dp) :: alpha = 1.0e-2_dp
    real(dp) :: lengths(2) = [1.0_dp, 1.0_dp]       ! array component, array constant
    character(len=16) :: solver = 'jacobi'
    integer(int32) :: steps                          ! deliberately not initialized
    real(dp), allocatable :: history(:)              ! always starts unallocated
  end type run_config_t

  type :: node_t
    integer(int32) :: value = 0_int32
    type(node_t), pointer :: next => null()          ! pointer components start disassociated
  end type node_t

contains

  subroutine defaults_demo(cfg)
    type(run_config_t), intent(out) :: cfg           ! intent(out) re-applies default initialization
    print '(a, i0, a, a)', 'inside: nx=', cfg%nx, ' solver=', trim(cfg%solver)
  end subroutine defaults_demo
end module heatflow_defaults

program default_initialization
  use heatflow_defaults
  implicit none
  type(run_config_t) :: a                              ! defaults applied at declaration
  type(run_config_t) :: b = run_config_t(nx=81, ny=81, alpha=2.0e-2_dp, lengths=[2.0_dp, 1.0_dp], &
                                         solver='ftcs', steps=100, history=null())
  type(run_config_t), parameter :: reference = run_config_t(steps=0)  ! named constant: defaults fill the rest
  type(node_t) :: head
  type(run_config_t) :: c

  print '(a, 2i4, es9.2, 2f5.1, 1x, a)', 'a (defaults):  ', a%nx, a%ny, a%alpha, a%lengths, trim(a%solver)
  print '(a, 2i4, es9.2, 2f5.1, 1x, a)', 'b (explicit):  ', b%nx, b%ny, b%alpha, b%lengths, trim(b%solver)
  print '(a, 2i4, es9.2, 2f5.1, 1x, a, i3)', 'reference:     ', reference%nx, reference%ny, reference%alpha, &
    reference%lengths, trim(reference%solver), reference%steps
  print '(a, l1, a, l1)', 'a%history allocated: ', allocated(a%history), '  head%next associated: ', associated(head%next)

  c%nx = 7                                              ! change a variable ...
  call defaults_demo(c)                                 ! ... intent(out) restores the defaults on entry
  print '(a, i0)', 'after the call c%nx = ', c%nx
end program default_initialization

Output:

a (defaults):    41  41 1.00E-02  1.0  1.0 jacobi
b (explicit):    81  81 2.00E-02  2.0  1.0 ftcs
reference:       41  41 1.00E-02  1.0  1.0 jacobi  0
a%history allocated: F  head%next associated: F
inside: nx=41 solver=jacobi
after the call c%nx = 41

The rules interact in ways that are easy to misjudge:

  • Explicit initialization overrides default initialization. b in the example is declared with an initializer, and that structure constructor gives a value for every component, so none of the defaults applies. The named constant reference is built from a constructor with only steps, and the defaults fill the rest.

  • A default initialization does not give the variable the save attribute. An initialization in a declaration does, and a default initialization in the type does not (7.5.4.6), so a local variable of a procedure is initialized again on every call. This is one of the practical differences between the two ways of writing an initial value.

  • intent(out) re-applies the defaults. The standard says that an intent(out) dummy argument becomes undefined on entry, "except for any subcomponents that are default-initialized" (8.5.10). In the example the caller set c%nx = 7, the subroutine saw nx=41, and the caller got 41 back. The practical consequence is that a procedure with an intent(out) argument of a type with defaults can use that type as a clean slate, and also that it cannot be used to preserve the old value of any defaulted component.

  • A partial initialization is allowed. steps has no default, so a%steps is undefined: reading it is an error that the compiler is not required to detect. Initialize the components that make sense, give the rest values in the constructor, and consider giving every component a default.

  • An initialization of a derived-type component can override the default of the component’s own type (7.5.4.6, Note 2).

  • A pointer component can be initialized to disassociated or to an initial target. => null() is by far the most common. A target must be a nonallocatable, noncoindexed variable with the target and save attributes (C770).

GNU Fortran has the option -finit-derived, which together with -finit-integer, -finit-real and its relatives fills the uninitialized components of derived-type variables with a pattern. It is a debugging aid, for example to make an uninitialized real component a signalling NaN with -finit-real=snan, and not a replacement for the defaults in the type definition. The manual says that these options do not initialize pointers, allocatable arrays or variables in an equivalence statement. LLVM Flang has -finit-local-zero for local variables without an explicit or default initialization; it also documents, as an extension, that a saved entity of a type with a default initializer has its components without defaults set to zero.

Custom constructors

The built-in constructor has three limits: it cannot enforce that components are consistent with each other, it needs every component to be accessible, and it cannot choose between representations. A module can add its own constructor by declaring a generic interface with the same name as the type (7.5.10, C7108). The type’s structure constructor and the specific procedures of the generic then share the name, and each reference is resolved to one of them.

module heatflow_material
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: dp, material_t

  integer, parameter :: dp = real64

  type :: material_t
    character(len=12) :: name = 'unnamed'
    real(dp) :: conductivity = 0.0_dp           ! W/(m K)
    real(dp) :: density = 0.0_dp                ! kg/m^3
    real(dp) :: specific_heat = 0.0_dp          ! J/(kg K)
    real(dp) :: diffusivity = 0.0_dp            ! derived: k/(rho c)
  end type material_t

  interface material_t                           ! a generic with the same name as the type
    module procedure new_material
  end interface material_t

contains

  function new_material(name, conductivity, density, specific_heat) result(m)
    character(len=*), intent(in) :: name
    real(dp), intent(in) :: conductivity, density, specific_heat
    type(material_t) :: m
    if (density <= 0.0_dp .or. specific_heat <= 0.0_dp) error stop 'material_t: density and specific heat must be positive'
    m%name = name
    m%conductivity = conductivity
    m%density = density
    m%specific_heat = specific_heat
    m%diffusivity = conductivity/(density*specific_heat)   ! keeps the derived component consistent
  end function new_material
end module heatflow_material

program custom_constructor
  use heatflow_material
  implicit none
  type(material_t) :: copper, raw

  copper = material_t('copper', 401.0_dp, 8960.0_dp, 385.0_dp)   ! generic: 4 arguments -> new_material
  print '(a, a, es10.3, a)', trim(copper%name), ' diffusivity=', copper%diffusivity, ' m2/s'

  ! The structure constructor is still there: five values (or keywords) do not match new_material.
  raw = material_t('copper', 401.0_dp, 8960.0_dp, 385.0_dp, 0.0_dp)
  print '(a, es10.3)', 'raw structure constructor diffusivity: ', raw%diffusivity
  raw = material_t(name='bare')
  print '(a, a, es10.3)', trim(raw%name), ' diffusivity=', raw%diffusivity
end program custom_constructor

Output:

copper diffusivity= 1.162E-04 m2/s
raw structure constructor diffusivity:  0.000E+00
bare diffusivity= 0.000E+00

material_t('copper', 401.0_dp, 8960.0_dp, 385.0_dp) has four arguments and matches new_material, which derives the diffusivity. The built-in constructor is still available for the argument lists that the specific procedure does not match: with five values, or with the keyword form material_t(name='bare'), the compiler chooses the structure constructor. The text of the standard (7.5.10, Note 1) is that name(...) is interpreted as a generic function reference when that is possible and as a structure constructor only otherwise; and C7108 forbids the case in which both would be valid.

That second rule matters when a specific procedure has the same signature as the structure constructor. The next example makes make_pair(a, b) match pair_t(1, 2) exactly. GNU Fortran and Flang both resolve the call to the function:

module pair_mod
  implicit none
  private
  public :: pair_t

  type :: pair_t
    integer :: a = 0
    integer :: b = 0
  end type pair_t

  interface pair_t
    module procedure make_pair
  end interface pair_t

contains

  function make_pair(a, b) result(p)             ! same signature as the structure constructor
    integer, intent(in) :: a, b
    type(pair_t) :: p
    p%a = a
    p%b = b + 1000                               ! visible proof that this function ran
  end function make_pair
end module pair_mod

program constructor_precedence
  use pair_mod
  implicit none
  type(pair_t) :: p
  p = pair_t(1, 2)
  print '(a, 2i6)', 'pair_t(1, 2)       -> ', p%a, p%b
  p = pair_t(a=1)                                ! make_pair needs b too: structure constructor
  print '(a, 2i6)', 'pair_t(a=1)        -> ', p%a, p%b
end program constructor_precedence

Output:

pair_t(1, 2)       ->      1  1002
pair_t(a=1)        ->      1     0

The function wins, which is why the guideline is to give the custom constructor a signature that the built-in one does not have, or to make the built-in one inaccessible by making the components private: users then cannot reach it either, and every value is built by the module’s code. A custom constructor is the natural place for validation (error stop or a stat argument), for computing derived components, and for allocating components.

A custom constructor can be an elemental or a pure function, can have optional arguments, and can be accompanied by other specific procedures with different argument lists. Because the generic has the same name as the type, one public statement exports both, and a private type name hides both.

Accessibility of components and types

Accessibility is declared at three independent levels (7.5.2.2 and 7.5.4.8): the type’s name, the components, and (for procedures) the type-bound procedures. The standard stresses that they are independent and that all four combinations of public and private names with public and private components are possible.

  • A private statement inside the type definition makes all components private by default and is allowed only in the specification part of a module (C771). Individual components can override it with public.

  • A private component is accessible only within the module that contains the definition and its descendant submodules. Outside, it cannot be named by % (so a program cannot read or write it) and it cannot be given a value in a structure constructor.

  • A private type name (type, private :: handle_t, or a bare private in the module) is accessible only inside the module. A public procedure may still take or return a value of that type, so callers can hold such values without being able to declare them.

The first example is the standard pattern for an opaque type: the type and its constructor are public, the components are private, and all access goes through procedures. label is made public because it is meant to be read.

module heatflow_probe
  use, intrinsic :: iso_fortran_env, only: real64, int32
  implicit none
  private
  public :: dp, probe_t, new_probe, record, mean_value, sample_count

  integer, parameter :: dp = real64

  type :: probe_t
    private                                    ! every component is private by default
    character(len=16), public :: label = ''   ! except this one
    integer(int32) :: n = 0_int32
    real(dp) :: total = 0.0_dp
  end type probe_t

contains

  function new_probe(label) result(p)
    character(len=*), intent(in) :: label
    type(probe_t) :: p
    p%label = label
  end function new_probe

  subroutine record(p, value)
    type(probe_t), intent(inout) :: p
    real(dp), intent(in) :: value
    p%n = p%n + 1_int32
    p%total = p%total + value
  end subroutine record

  pure function sample_count(p) result(n)       ! read-only accessor
    type(probe_t), intent(in) :: p
    integer(int32) :: n
    n = p%n
  end function sample_count

  pure function mean_value(p) result(m)
    type(probe_t), intent(in) :: p
    real(dp) :: m
    m = merge(p%total/real(p%n, dp), 0.0_dp, p%n > 0)
  end function mean_value
end module heatflow_probe

program information_hiding
  use heatflow_probe
  implicit none
  type(probe_t) :: p
  p = new_probe('centre')
  call record(p, 20.0_dp)
  call record(p, 30.0_dp)
  print '(a, a, a, i0, a, f5.1)', 'probe ', trim(p%label), ': n=', sample_count(p), ' mean=', mean_value(p)
end program information_hiding

Output:

probe centre: n=2 mean= 25.0

The following program is deliberately non-conforming. A module that defines a type with private components is used by a program that tries to construct the type with the built-in constructor, assign a component and read a component.

! INTENTIONALLY NON-CONFORMING: private components and the structure constructor.
module heatflow_probe
  implicit none
  private
  public :: probe_t
  type :: probe_t
    private
    integer :: n = 0
    real :: total = 0.0
  end type probe_t
end module heatflow_probe

program private_misuse
  use heatflow_probe
  implicit none
  type(probe_t) :: p, q
  q = probe_t(n=1, total=2.0)   ! error: the components are not accessible here
  p%n = 3                       ! error: private component
  print *, p%total              ! error: private component
end program private_misuse

GNU Fortran 16.2:

04b_private_errors.f90:18:6:

   18 |   p%n = 3                       ! error: private component
      |      1
Error: Component 'n' at (1) is a PRIVATE component of 'probe_t'
04b_private_errors.f90:19:19:

   19 |   print *, p%total              ! error: private component
      |                   1
Error: Component 'total' at (1) is a PRIVATE component of 'probe_t'
04b_private_errors.f90:17:7:

   17 |   q = probe_t(n=1, total=2.0)   ! error: the components are not accessible here
      |       1
Error: Component 'n' at (1) is a PRIVATE component of 'probe_t'

LLVM Flang 23:

error: Semantic errors in 04b_private_errors.f90
04b_private_errors.f90:17:17: error: PRIVATE name 'n' is accessible only within module 'heatflow_probe'
    q = probe_t(n=1, total=2.0)   ! error: the components are not accessible here
                  ^
04b_private_errors.f90:17:26: error: PRIVATE name 'total' is accessible only within module 'heatflow_probe'
    q = probe_t(n=1, total=2.0)   ! error: the components are not accessible here
                           ^^^
04b_private_errors.f90:18:5: error: PRIVATE name 'n' is accessible only within module 'heatflow_probe'
    p%n = 3                       ! error: private component
      ^
04b_private_errors.f90:19:14: error: PRIVATE name 'total' is accessible only within module 'heatflow_probe'
    print *, p%total              ! error: private component
               ^^^^^

Unlike a private module variable, which GNU Fortran reports as "has no IMPLICIT type" under implicit none, a private component is reported by its real name, because the component name is looked up in the type, which is accessible.

Making the type name private is a different statement of intent, as the next deliberately non-conforming program shows. A public function can return a value of a private type, and a caller can pass it on, but a declaration in the program cannot name the type:

! INTENTIONALLY NON-CONFORMING: a private type name cannot be used outside its module.
module heatflow_handles
  implicit none
  private
  public :: open_handle, handle_value

  type :: handle_t                       ! the type name is private, its components are public
    integer :: value = 0
  end type handle_t

contains

  function open_handle(v) result(h)
    integer, intent(in) :: v
    type(handle_t) :: h                  ! a public function may return a private type
    h%value = v
  end function open_handle

  integer function handle_value(h)
    type(handle_t), intent(in) :: h
    handle_value = h%value
  end function handle_value
end module heatflow_handles

program private_type_name
  use heatflow_handles
  implicit none
  type(handle_t) :: h                      ! error: handle_t is not accessible here
  print *, handle_value(open_handle(42))   ! fine on its own: no declaration names the type
end program private_type_name

GNU Fortran 16.2:

04c_private_type.f90:28:17:

   28 |   type(handle_t) :: h                      ! error: handle_t is not accessible here
      |                 1
Error: Derived type 'handle_t' at (1) is being used before it is defined

LLVM Flang 23:

error: Semantic errors in 04c_private_type.f90
04c_private_type.f90:28:8: error: Derived type 'handle_t' not found
    type(handle_t) :: h                      ! error: handle_t is not accessible here
         ^^^^^^^^
04c_private_type.f90:28:21: error: No explicit type declared for 'h'
    type(handle_t) :: h                      ! error: handle_t is not accessible here
                      ^

With the offending declaration removed, the program prints 42 on both compilers. The GNU Fortran message, "being used before it is defined", is misleading, but the declaration is the cause: the name of the type is not accessible. The usual reason to want a private type name is an opaque handle (the caller must not construct or inspect the value), but a type with a public name and private components achieves that and also lets callers declare variables, so it is the more common choice.

Intrinsic assignment, copies and equality

a = b is defined for two entities of the same derived type (10.2.1.2) without any code from the programmer. It is performed as if each component were assigned (10.2.1.3), and the kind of component decides what that means:

Component What a = b does to it

Nonpointer, nonallocatable of intrinsic type

ordinary intrinsic assignment, with conversion and padding rules

Nonpointer, nonallocatable of derived type

intrinsic assignment of that type, or its type-bound defined assignment if it has one that is consistent with the component

Pointer

pointer assignment: the receiving pointer becomes associated with the same target as the source; nothing is copied

Allocatable

if the receiving component is allocated it is deallocated; if the source component is allocated, the receiver is allocated with the same dynamic type, type parameters and bounds, and the value is copied; if the source is unallocated the receiver ends unallocated

Coarray allocatable

intrinsic assignment, with the extra rule that an unallocated source requires an unallocated target

So a derived type is a value type as long as it has only nonpointer components, and a shared-reference type as soon as it has a pointer component. The standard also forbids intrinsic assignment when the variable is polymorphic and not allocatable (10.2.1.2), a restriction that matters in the object-oriented pages. The example below puts an allocatable component and a pointer component in one type and shows what the copy shares and what it does not.

module heatflow_snapshot
  use, intrinsic :: iso_fortran_env, only: real64, int32
  implicit none
  private
  public :: dp, snapshot_t

  integer, parameter :: dp = real64

  type :: snapshot_t
    integer(int32) :: step = 0_int32
    real(dp), allocatable :: u(:)               ! owned data: copied by assignment
    real(dp), pointer :: view(:) => null()      ! borrowed data: only the pointer is copied
  end type snapshot_t
end module heatflow_snapshot

program derived_type_assignment
  use heatflow_snapshot
  implicit none
  real(dp), target :: shared(3) = [1.0_dp, 2.0_dp, 3.0_dp]
  type(snapshot_t) :: a, b

  a%step = 1
  a%u = [10.0_dp, 20.0_dp, 30.0_dp]
  a%view => shared

  b = a                                          ! intrinsic assignment, component by component
  print '(a, l1, a, i0)', 'b%u allocated: ', allocated(b%u), '  size=', size(b%u)
  print '(a, l1)', 'pointer components associated with the same target? ', associated(a%view, b%view)

  a%u(1) = -1.0_dp                               ! does not touch b%u: the allocatable component was copied
  a%view(1) = -1.0_dp                            ! changes shared(1), seen through b%view as well
  print '(a, 3f6.1)', 'a%u    = ', a%u
  print '(a, 3f6.1)', 'b%u    = ', b%u
  print '(a, 3f6.1)', 'b%view = ', b%view

  deallocate (a%u)                               ! b is not affected
  print '(a, l1, a, l1)', 'a%u allocated: ', allocated(a%u), '  b%u allocated: ', allocated(b%u)

end program derived_type_assignment

Output:

b%u allocated: T  size=3
pointer components associated with the same target? T
a%u    =   -1.0  20.0  30.0
b%u    =   10.0  20.0  30.0
b%view =   -1.0   2.0   3.0
a%u allocated: F  b%u allocated: T

The changes to a%u do not reach b%u, and deallocate (a%u) leaves b%u allocated, because the copy has its own storage. The write through a%view changes shared, which b%view also sees, because only the association was copied. When a type contains pointer components that represent ownership (the structure allocated them), the shallow copy is a latent double deallocation. Make such components allocatable so that assignment deep-copies, or write a type-bound defined assignment, see Type-Bound Procedures, Abstract Types and Finalization.

No intrinsic comparison

There are no intrinsic operations on derived types other than assignment (7.5.11). In particular == and /= do not exist, so this deliberately non-conforming program is rejected:

! INTENTIONALLY NON-CONFORMING: there is no intrinsic == for derived types.
program no_equality
  implicit none
  type :: point_t
    real :: x, y
  end type point_t
  type(point_t) :: a, b
  a = point_t(1.0, 2.0)
  b = point_t(1.0, 2.0)
  if (a == b) print *, 'equal'
end program no_equality

GNU Fortran 16.2:

05b_no_equality.f90:10:6:

   10 |   if (a == b) print *, 'equal'
      |      1
Error: Operands of comparison operator '==' at (1) are TYPE(point_t)/TYPE(point_t)

LLVM Flang 23:

error: Semantic errors in 05b_no_equality.f90
05b_no_equality.f90:10:7: error: Operands of .EQ. must have comparable types; have TYPE(point_t) and TYPE(point_t)
    if (a == b) print *, 'equal'
        ^^^^^^

Operations are defined with a generic interface for the operator (15.4.3.4.2) and defined assignment is a subroutine with assignment(=). Both are covered in Interfaces, Generics and Operator Overloading; the short form follows. Comparing the real components of a structure for exact equality is rarely what is wanted, and -Wextra warns about it (-Wcompare-reals), so the function uses a tolerance. An elemental function makes the operator work on arrays of structures for free.

module heatflow_points
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: dp, point_t, operator(==), operator(-)

  integer, parameter :: dp = real64
  real(dp), parameter :: tolerance = 1.0e-12_dp   ! equality of reals is a tolerance test, not a bit test

  type :: point_t
    real(dp) :: x = 0.0_dp
    real(dp) :: y = 0.0_dp
  end type point_t

  interface operator(==)                      ! intrinsic == does not exist for derived types
    module procedure points_equal
  end interface

  interface operator(-)
    module procedure points_difference
  end interface

contains

  elemental logical function points_equal(a, b)
    type(point_t), intent(in) :: a, b
    points_equal = abs(a%x - b%x) <= tolerance .and. abs(a%y - b%y) <= tolerance
  end function points_equal

  elemental function points_difference(a, b) result(d)
    type(point_t), intent(in) :: a, b
    type(point_t) :: d
    d = point_t(a%x - b%x, a%y - b%y)
  end function points_difference
end module heatflow_points

program derived_type_equality
  use heatflow_points
  implicit none
  type(point_t) :: a, b(3)
  a = point_t(1.0_dp, 2.0_dp)
  b = [point_t(1.0_dp, 2.0_dp), point_t(0.0_dp, 0.0_dp), point_t(1.0_dp, 2.0_dp)]
  print '(a, l1)', 'a == b(1): ', a == b(1)
  print '(a, 3l2)', 'elemental on an array: ', a == b
  print '(a, 2f5.1)', 'b(2) - a: ', b(2) - a
end program derived_type_equality

Output:

a == b(1): T
elemental on an array:  T F T
b(2) - a:  -1.0 -2.0

Note that operator(==) and operator(-) are listed in the public statement of the module: operators are generic identifiers and have their own accessibility, so a private module default hides them unless they are exported.

Recursive types

A type cannot contain itself directly, because it would have infinite size, but a component that is a pointer or is allocatable can have the type being defined (C749 requires a previously defined type only for components that are neither). There are two ways to build a list or a tree.

  • A pointer component, type(node_t), pointer :: next => null(), is the oldest form: pointer components date from Fortran 90, and the => null() default initialization from Fortran 95. Nodes are allocated one at a time with allocate (p), linked with p%next => q, and freed explicitly, one at a time. Assignment of a node copies only the pointer, so two lists share their tails. The full treatment, including leaks and dangling pointers, is in Pointers and Targets.

  • An allocatable component, type(node_t), allocatable :: next, is the Fortran 2008 form. The object owns its successor: deallocate (head) frees the whole chain, and assignment must copy the whole chain. move_alloc rearranges nodes without copying.

The next program builds a chain of sample temperatures with the allocatable form. push_front uses move_alloc to hand the old chain to the new node, and the traversals are recursive functions, which is the natural way to walk a type that is itself recursive.

module heatflow_samples
  use, intrinsic :: iso_fortran_env, only: real64, int32
  implicit none
  private
  public :: dp, node_t, sample_list_t, push_front, count_nodes, sum_nodes

  integer, parameter :: dp = real64

  type :: node_t                                 ! allocatable recursive component
    real(dp) :: temperature = 0.0_dp
    type(node_t), allocatable :: next
  end type node_t

  type :: sample_list_t
    type(node_t), allocatable :: head
  end type sample_list_t

contains

  subroutine push_front(list, temperature)
    type(sample_list_t), intent(inout) :: list
    real(dp), intent(in) :: temperature
    type(node_t), allocatable :: fresh
    allocate (fresh)
    fresh%temperature = temperature
    call move_alloc(list%head, fresh%next)      ! the old chain becomes the tail, no copy
    call move_alloc(fresh, list%head)
  end subroutine push_front

  function count_nodes(list) result(n)
    type(sample_list_t), intent(in) :: list
    integer(int32) :: n
    n = 0_int32
    if (allocated(list%head)) n = count_from(list%head)
  end function count_nodes

  function sum_nodes(list) result(total)
    type(sample_list_t), intent(in) :: list
    real(dp) :: total
    total = 0.0_dp
    if (allocated(list%head)) total = sum_from(list%head)
  end function sum_nodes

  recursive function sum_from(node) result(total)
    type(node_t), intent(in) :: node
    real(dp) :: total
    total = node%temperature
    if (allocated(node%next)) total = total + sum_from(node%next)
  end function sum_from

  recursive function count_from(node) result(n)
    type(node_t), intent(in) :: node
    integer(int32) :: n
    n = 1_int32
    if (allocated(node%next)) n = n + count_from(node%next)
  end function count_from
end module heatflow_samples

program recursive_types
  use heatflow_samples
  implicit none
  type(sample_list_t) :: list
  integer :: i

  do i = 1, 5
    call push_front(list, 20.0_dp + real(i, dp))
  end do
  print '(a, i0, a, f5.1)', 'nodes: ', count_nodes(list), '  newest=', list%head%temperature
  print '(a, f6.1)', 'sum of temperatures: ', sum_nodes(list)

  deallocate (list%head)                         ! deallocates every node, recursively
  print '(a, l1, a, i0)', 'head allocated: ', allocated(list%head), '  nodes: ', count_nodes(list)
end program recursive_types

Output:

nodes: 5  newest= 25.0
sum of temperatures:  115.0
head allocated: F  nodes: 0

Two costs come with the allocatable form. Deallocating or copying a chain recurses once per node, so a list with millions of nodes can overflow the stack; and each node is a separate heap allocation, so a list is a poor container for numeric data and an array of nodes with integer indices, or a plain allocatable array, is usually better.

A compiler defect: copies of long recursive chains

Assignment of a type with a recursive allocatable component must copy the whole chain (10.2.1.3 step 2). On GNU Fortran 16.2 it does not. This program, which is conforming, builds a chain of four nodes, copies the container, overwrites every level of the original and prints the copy:

module heatflow_chain
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: dp, node_t, chain_t, append_value

  integer, parameter :: dp = real64

  type :: node_t
    real(dp) :: temperature = 0.0_dp
    type(node_t), allocatable :: next
  end type node_t

  type :: chain_t
    type(node_t), allocatable :: head
  end type chain_t

contains

  subroutine append_value(chain, temperature)    ! walks to the end by recursion
    type(chain_t), intent(inout) :: chain
    real(dp), intent(in) :: temperature
    if (.not. allocated(chain%head)) then
      allocate (chain%head)
      chain%head%temperature = temperature
    else
      call append_node(chain%head, temperature)
    end if
  end subroutine append_value

  recursive subroutine append_node(node, temperature)
    type(node_t), intent(inout) :: node
    real(dp), intent(in) :: temperature
    if (allocated(node%next)) then
      call append_node(node%next, temperature)
    else
      allocate (node%next)
      node%next%temperature = temperature
    end if
  end subroutine append_node
end module heatflow_chain

program copy_depth
  use heatflow_chain
  implicit none
  type(chain_t) :: a, b
  integer :: i

  do i = 1, 4
    call append_value(a, real(i, dp))            ! the chain holds 1.0, 2.0, 3.0, 4.0
  end do

  b = a                                          ! intrinsic assignment: deep copy required by 10.2.1.3
  a%head%temperature = -1.0_dp                   ! overwrite every level of a
  a%head%next%temperature = -2.0_dp
  a%head%next%next%temperature = -3.0_dp
  a%head%next%next%next%temperature = -4.0_dp

  print '(a, 4f6.1)', 'b after changing a: ', b%head%temperature, b%head%next%temperature, &
    b%head%next%next%temperature, b%head%next%next%next%temperature
end program copy_depth

GNU Fortran 16.2:

b after changing a:    1.0   2.0  -3.0  -4.0

LLVM Flang 23:

b after changing a:    1.0   2.0   3.0   4.0

GNU Fortran copied the first two levels and shared the rest, so the copy shows -3.0 and -4.0; Flang printed the expected 1.0 2.0 3.0 4.0. A variant of this program that went on to deallocate (a%head) and then read the copy crashed with a segmentation fault, because the deeper nodes had been freed together with the original. The same defect is described for a chain of three nodes in Pointers and Targets. A workaround that works on both compilers is to give the node type a type-bound defined assignment that copies one node and calls itself for the next, and to invoke it on the node rather than on the container (in this test GNU Fortran did not use the component’s defined assignment when the container was assigned with =):

module heatflow_chain
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: dp, node_t, chain_t, append_value

  integer, parameter :: dp = real64

  type :: node_t
    real(dp) :: temperature = 0.0_dp
    type(node_t), allocatable :: next
  contains
    procedure, private :: assign_node
    generic :: assignment(=) => assign_node      ! defined assignment: the component is copied level by level
  end type node_t

  type :: chain_t
    type(node_t), allocatable :: head
  end type chain_t

contains

  recursive subroutine assign_node(lhs, rhs)
    class(node_t), intent(out) :: lhs
    class(node_t), intent(in) :: rhs
    lhs%temperature = rhs%temperature
    if (allocated(rhs%next)) then
      allocate (lhs%next)
      lhs%next = rhs%next                        ! recursion through the defined assignment
    end if
  end subroutine assign_node

  subroutine append_value(chain, temperature)
    type(chain_t), intent(inout) :: chain
    real(dp), intent(in) :: temperature
    if (.not. allocated(chain%head)) then
      allocate (chain%head)
      chain%head%temperature = temperature
    else
      call append_node(chain%head, temperature)
    end if
  end subroutine append_value

  recursive subroutine append_node(node, temperature)
    type(node_t), intent(inout) :: node
    real(dp), intent(in) :: temperature
    if (allocated(node%next)) then
      call append_node(node%next, temperature)
    else
      allocate (node%next)
      node%next%temperature = temperature
    end if
  end subroutine append_node
end module heatflow_chain

program copy_depth_fixed
  use heatflow_chain
  implicit none
  type(chain_t) :: a, b
  integer :: i

  do i = 1, 4
    call append_value(a, real(i, dp))
  end do

  allocate (b%head)
  b%head = a%head                                ! call the defined assignment on the node itself
  a%head%temperature = -1.0_dp
  a%head%next%temperature = -2.0_dp
  a%head%next%next%temperature = -3.0_dp
  a%head%next%next%next%temperature = -4.0_dp

  print '(a, 4f6.1)', 'b after changing a: ', b%head%temperature, b%head%next%temperature, &
    b%head%next%next%temperature, b%head%next%next%next%temperature
  deallocate (a%head)
  print '(a, f4.1)', 'tail of b after deallocating a: ', b%head%next%next%next%temperature
end program copy_depth_fixed

Output on both compilers:

b after changing a:    1.0   2.0   3.0   4.0
tail of b after deallocating a:  4.0

Until the compiler is fixed, treat deep copies of recursive allocatable chains as something to test on every compiler you support, or avoid them by passing chains by reference and moving them with move_alloc.

Arrays of structures and structures of arrays

An array of derived-type values is declared like any other array, and a component selector applied to it yields an array of the components: row%temperature is the real array of the temperatures of all the elements, it can be assigned to as a whole (row%temperature = [...]), used with a vector subscript (row([1, 5])%fixed = .true.), or used as a mask. The rank rule (C919) allows only one part of the designator to have a nonzero rank, so soa%a(1:2)%b is accepted only when one of the two is scalar.

The choice between an array of structures (an array of cell_t, one record per cell) and a structure of arrays (one cells_t with one array per field) is a layout decision with performance consequences: the first keeps the fields of one element together, the second keeps one field of all elements together, which is what vectorized loops and stencils want. Performance and Memory Layout explains the trade-off. The program shows the access syntax of both and converts one into the other.

module heatflow_cells
  use, intrinsic :: iso_fortran_env, only: real64, int32
  implicit none
  private
  public :: dp, cell_t, cells_t, hottest

  integer, parameter :: dp = real64

  type :: cell_t                             ! array of structures: one record per cell
    real(dp) :: temperature = 0.0_dp
    real(dp) :: conductivity = 1.0_dp
    logical :: fixed = .false.
  end type cell_t

  type :: cells_t                            ! structure of arrays: one array per field
    real(dp), allocatable :: temperature(:)
    real(dp), allocatable :: conductivity(:)
    logical, allocatable :: fixed(:)
  end type cells_t

contains

  pure function hottest(c) result(i)
    type(cell_t), intent(in) :: c(:)
    integer(int32) :: i
    i = int(maxloc(c%temperature, dim=1), int32)   ! a component of an array of structures is an array
  end function hottest
end module heatflow_cells

program arrays_of_structures
  use heatflow_cells
  implicit none
  type(cell_t) :: row(5)
  type(cells_t) :: soa
  integer :: i

  row%temperature = [20.0_dp, 25.0_dp, 80.0_dp, 25.0_dp, 20.0_dp]   ! assign a component of every element
  row([1, 5])%fixed = .true.                                        ! vector subscript on the structure array
  row(2:4)%conductivity = [1.0_dp, 2.0_dp, 1.0_dp]

  print '(a, i0)', 'hottest cell: ', hottest(row)
  print '(a, 5l2)', 'fixed:        ', row%fixed
  print '(a, f6.1)', 'mean of the free cells: ', sum(row%temperature, mask=.not. row%fixed)/count(.not. row%fixed)

  soa%temperature = row%temperature                                ! SoA from AoS, one component at a time
  soa%conductivity = row%conductivity
  soa%fixed = row%fixed
  print '(a, 5f6.1)', 'soa%temperature: ', soa%temperature
  do i = 1, size(soa%temperature)
    if (.not. soa%fixed(i)) soa%temperature(i) = soa%temperature(i) + 1.0_dp
  end do
  print '(a, 5f6.1)', 'after the update:', soa%temperature
end program arrays_of_structures

Output:

hottest cell: 3
fixed:         T F F F T
mean of the free cells:   43.3
soa%temperature:   20.0  25.0  80.0  25.0  20.0
after the update:  20.0  26.0  81.0  26.0  20.0

hottest receives the array of structures as an assumed-shape dummy argument and uses maxloc over a component, which works without copying: the selector c%temperature of an assumed-shape array is a strided section.

The conversion from the array of structures to the structure of arrays is written as three assignments rather than a single structure constructor on purpose. On GNU Fortran 16.2 this valid program, a constructor whose allocatable component receives an array of components, prints the correct size and sum but fails when the array itself is written:

program aos_constructor
  implicit none
  type :: cell_t
    real :: temperature = 0.0
  end type cell_t
  type :: cells_t
    real, allocatable :: temperature(:)
  end type cells_t
  type(cell_t) :: row(3)
  type(cells_t) :: soa

  row%temperature = [1.0, 2.0, 3.0]
  soa = cells_t(temperature=row%temperature)       ! valid: an array expression for an allocatable component
  print *, 'size=', size(soa%temperature), ' sum=', sum(soa%temperature)
  print *, soa%temperature
end program aos_constructor

GNU Fortran 16.2 (backtrace frames omitted):

At line 15 of file 08b_aos_constructor_defect.f90 (unit = 6, file = 'stdout')
Internal Error: list_formatted_write(): Bad type
 size=           3  sum=   6.00000000

LLVM Flang 23:

 size= 3  sum= 6.
 1. 2. 3.

The descriptor that the constructor stored in soa%temperature evidently lacks the element type, so list-directed output stops with an internal error; assigning the components one at a time (soa%temperature = row%temperature), as in the main example, or constructing from a temporary array, avoids it. The defect was seen on the GNU Fortran 16.2 used for this page and may be fixed in later releases.

Procedure pointer components

A component can be a pointer to a procedure (R741). It must have the pointer attribute (C761), the access attributes public or private, and, when the interface is implicit or has no arguments, nopass (C762); otherwise the first argument of the target is the passed-object dummy argument that receives the structure itself (pass is the default, pass(name) selects another argument, C763). Procedure pointer components make a structure carry behaviour that can be chosen at run time, which is the "strategy" pattern; the fixed alternative is a type-bound procedure, see Type-Bound Procedures, Abstract Types and Finalization.

module heatflow_sources
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: dp, source_t, constant_source, pulse_source

  integer, parameter :: dp = real64

  abstract interface
    pure function source_fn(t) result(q)     ! heat source as a function of time
      import :: dp
      real(dp), intent(in) :: t
      real(dp) :: q
    end function source_fn
  end interface

  type :: source_t
    character(len=12) :: name = 'none'
    procedure(source_fn), pointer, nopass :: rate => null()   ! procedure pointer component
  end type source_t

contains

  pure function constant_source(t) result(q)
    real(dp), intent(in) :: t
    real(dp) :: q
    q = 5.0_dp + 0.0_dp*t
  end function constant_source

  pure function pulse_source(t) result(q)
    real(dp), intent(in) :: t
    real(dp) :: q
    q = merge(100.0_dp, 0.0_dp, t < 0.5_dp)
  end function pulse_source
end module heatflow_sources

program procedure_components
  use heatflow_sources
  implicit none
  type(source_t) :: s(2), empty

  s(1) = source_t('constant', constant_source)       ! a procedure as a structure constructor value
  s(2) = source_t(name='pulse', rate=pulse_source)
  print '(a, f6.1, f6.1)', s(1)%name, s(1)%rate(0.0_dp), s(1)%rate(1.0_dp)
  print '(a, f6.1, f6.1)', s(2)%name, s(2)%rate(0.0_dp), s(2)%rate(1.0_dp)
  print '(a, l1)', 'default component associated: ', associated(empty%rate)
end program procedure_components

Output:

constant       5.0   5.0
pulse        100.0   0.0
default component associated: F

A structure constructor takes a procedure as the value of a procedure pointer component (a proc-target, C7109), either by position or by keyword. A procedure pointer component without an initializer is undefined; here it is initialized with => null(). Calling a disassociated procedure pointer is an error that, as for data pointers, the compiler is not required to detect.

Storage: sequence types, bind(c) types and layout

By default the standard says nothing about how components are stored: the processor may reorder them, pad them and keep them in any form (7.5.1). The order of declaration matters only for the places that name the component order, which are structure constructors without keywords, intrinsic assignment, namelist input and the expansion of a structure in formatted or list-directed I/O (7.5.2.3, Note 2). Two statements give stronger guarantees.

  • sequence (R731, 7.5.2.3) makes the order of the components the storage order, and makes the type usable in common and equivalence with the storage association rules of 19.5.3. Its constraints are strict (C745): at least one component, every data component of intrinsic type or of a sequence type, no type parameters and no type-bound procedure part; and a sequence type cannot be extended (C740). It does not remove padding between components.

  • bind(c) (7.5.2, 18.3.4) makes the type interoperable with a C struct: the layout is the one the companion C processor uses. The constraints (C1801 to C1806) are: no sequence, no type parameters, no extends, no type-bound procedures, at least one component, and every component a nonpointer, nonallocatable data component of interoperable type. Anything with a descriptor or a length that C does not know is excluded.

Property sequence type bind(c) type

Storage order

order of declaration

as C lays out the same members

Component types

intrinsic or sequence types, any kind

interoperable types only: c_int, c_double, c_char, …​ or other bind(c) types

Allocatable and pointer components

allowed when the type is intrinsic or a sequence type (C745), but the type is then not a numeric or character sequence type

none

Extension, type-bound procedures, parameters

none

none

Use

storage association, same-type rule across scopes, transfer

calls to and from C, see Interoperability with C

The program shows a sequence type used on both sides of an interface body (see the same-type rule in the next section) and what storage_size reports for it and for an ordinary type with the same components. In both GNU Fortran and Flang the two sizes are equal, and both exceed the sum of the components, which shows the padding that sequence does not remove.

program sequence_types
  use, intrinsic :: iso_fortran_env, only: int32, real32, int8
  implicit none

  type :: reading_t                       ! a SEQUENCE type: order of components is the storage order
    sequence
    integer(int32) :: id
    real(real32) :: value
    integer(int8) :: flag
  end type reading_t

  type :: plain_t                         ! an ordinary type: storage order and padding are processor-dependent
    integer(int32) :: id
    real(real32) :: value
    integer(int8) :: flag
  end type plain_t

  interface
    subroutine show(r)                    ! the interface body repeats the SEQUENCE definition: same type
      use, intrinsic :: iso_fortran_env, only: int32, real32, int8
      type :: reading_t
        sequence
        integer(int32) :: id
        real(real32) :: value
        integer(int8) :: flag
      end type reading_t
      type(reading_t), intent(in) :: r
    end subroutine show
  end interface

  type(reading_t) :: r
  type(plain_t) :: p

  r = reading_t(7_int32, 21.5_real32, 1_int8)
  p = plain_t(7_int32, 21.5_real32, 1_int8)
  call show(r)
  print '(a, i0, a, i0)', 'storage_size in bits, sequence type: ', storage_size(r), '  plain type: ', storage_size(p)
  print '(a, i0)', 'sum of the component sizes in bits: ', storage_size(r%id) + storage_size(r%value) + storage_size(r%flag)
end program sequence_types

subroutine show(r)
  use, intrinsic :: iso_fortran_env, only: int32, real32, int8
  implicit none
  type :: reading_t
    sequence
    integer(int32) :: id
    real(real32) :: value
    integer(int8) :: flag
  end type reading_t
  type(reading_t), intent(in) :: r
  print '(a, i0, a, f5.1, a, i0)', 'show: id=', r%id, ' value=', r%value, ' flag=', r%flag
end subroutine show

Output:

show: id=7 value= 21.5 flag=1
storage_size in bits, sequence type: 96  plain type: 96
sum of the component sizes in bits: 72

The sum of the component sizes is 72 bits (32 plus 32 plus 8), and the object takes 96 bits: three bytes of padding align the 4-byte members. storage_size returns the size in bits of an element, and it is the portable way to ask; it is processor dependent for ordinary types.

The bind(c) form is tested against a real C compiler in the next program. The C function receives a pointer to the struct, doubles one member and returns sizeof the struct; c_sizeof gives the same number on the Fortran side. The Fortran interface declares the dummy argument as type(heatflow_cell_t), intent(inout), which is passed by reference, that is, as the C pointer.

#include <stddef.h>

typedef struct {
  int id;
  double temperature;
  double position[2];
} heatflow_cell_t;

/* Doubles the temperature of one cell and returns the size of the struct. */
size_t heatflow_heat_cell(heatflow_cell_t *c) {
  c->temperature *= 2.0;
  return sizeof(heatflow_cell_t);
}
program bind_c_type
  use, intrinsic :: iso_c_binding, only: c_int, c_double, c_size_t, c_sizeof
  implicit none

  type, bind(c) :: heatflow_cell_t                ! interoperable with the C struct of the same layout
    integer(c_int) :: id
    real(c_double) :: temperature
    real(c_double) :: position(2)
  end type heatflow_cell_t

  interface
    function heatflow_heat_cell(c) bind(c, name='heatflow_heat_cell') result(nbytes)
      import :: heatflow_cell_t, c_size_t
      type(heatflow_cell_t), intent(inout) :: c   ! passed by reference: a C pointer to the struct
      integer(c_size_t) :: nbytes
    end function heatflow_heat_cell
  end interface

  type(heatflow_cell_t) :: cell
  integer(c_size_t) :: c_size

  cell = heatflow_cell_t(id=1_c_int, temperature=21.5_c_double, position=[0.25_c_double, 0.75_c_double])
  c_size = heatflow_heat_cell(cell)
  print '(a, f5.1)', 'temperature after the C call: ', cell%temperature
  print '(a, i0, a, i0)', 'sizeof in C: ', c_size, '   c_sizeof in Fortran: ', c_sizeof(cell)
end program bind_c_type

Built with gcc -c 15_cell.c and gfortran 15_bind_c_type.f90 15_cell.o (strict flags as above), and run:

temperature after the C call:  43.0
sizeof in C: 32   c_sizeof in Fortran: 32

Flang 23 linked against the same object file printed the same two lines.

The constraints are enforced. This deliberately non-conforming module has an allocatable component, a pointer component, a component of default real, and an empty bind(c) type:

! INTENTIONALLY NON-CONFORMING: components that cannot be in a BIND(C) type.
module bad
  use, intrinsic :: iso_c_binding, only: c_double
  implicit none
  type, bind(c) :: cell_t
    real(c_double), allocatable :: history(:)   ! error: allocatable
    real :: z                                    ! only a warning: the kind is not named by iso_c_binding
    integer, pointer :: p                        ! error: pointer
  end type cell_t
  type, bind(c) :: empty_t                       ! error: no components
  end type empty_t
end module bad

GNU Fortran 16.2:

15b_bind_c_errors.f90:6:45:

    5 |   type, bind(c) :: cell_t
      |                         2
    6 |     real(c_double), allocatable :: history(:)   ! error: allocatable
      |                                             1
Error: Component 'history' at (1) cannot have the ALLOCATABLE attribute because it is a member of the BIND(C) derived type 'cell_t' at (2)
15b_bind_c_errors.f90:7:13:

    7 |     real :: z                                    ! only a warning: the kind is not named by iso_c_binding
      |             1
Warning: Component 'z' in derived type 'cell_t' at (1) may not be C interoperable, even though derived type 'cell_t' is BIND(C) [-Wc-binding-type]
15b_bind_c_errors.f90:8:25:

    5 |   type, bind(c) :: cell_t
      |                         2
......
    8 |     integer, pointer :: p                        ! error: pointer
      |                         1
Error: Component 'p' at (1) cannot have the POINTER attribute because it is a member of the BIND(C) derived type 'cell_t' at (2)
15b_bind_c_errors.f90:8:25:

    8 |     integer, pointer :: p                        ! error: pointer
      |                         1
Warning: Component 'p' in derived type 'cell_t' at (1) may not be C interoperable, even though derived type 'cell_t' is BIND(C) [-Wc-binding-type]
15b_bind_c_errors.f90:10:71:

   10 |   type, bind(c) :: empty_t                       ! error: no components
      |                                                                       1
Error: GNU Extension: Derived type 'empty_t' with BIND(C) attribute at (1) has no components

LLVM Flang 23 rejects the allocatable and pointer components and does not mention the others:

error: Semantic errors in 15b_bind_c_errors.f90
15b_bind_c_errors.f90:6:36: error: An interoperable derived type cannot have a pointer or allocatable component
      real(c_double), allocatable :: history(:)   ! error: allocatable
                                     ^^^^^^^
15b_bind_c_errors.f90:8:25: error: An interoperable derived type cannot have a pointer or allocatable component
      integer, pointer :: p                        ! error: pointer
                          ^

GNU Fortran 16.2 with -std=f2023 treats the empty bind(c) type as an error ("GNU Extension") because C1805 requires a component, and it warns, under -Wc-binding-type, that a component whose kind is spelled real instead of real(c_float) "may not be C interoperable". The warning is a reminder to use the kind constants of iso_c_binding instead of default kinds, even though the default real happens to be the same as c_float on most processors. An ordinary type with no components, in contrast, is allowed:

program empty_type
  implicit none
  type :: tag_t                       ! a type with no components is allowed
  end type tag_t
  type :: c_t
    type(c_t), pointer :: next => null()    ! fine: a pointer to the type being defined
  end type c_t
  type(tag_t) :: t
  type(c_t) :: c
  print '(a, l1, a, i0)', 'next associated: ', associated(c%next), '  storage_size of an empty type in bits: ', storage_size(t)
end program empty_type

Output:

next associated: F  storage_size of an empty type in bits: 0

Are two types the same type?

Two definitions with the same name are two types, with one exception (7.5.2.4). Declarations with reference to the same definition, reached by use or host association, name the same type, even through renaming. Definitions in different scoping units with the same name are the same type only if all of them are sequence types or all are bind(c) types, none has a private component, and the components agree in order, name and attributes. That is how the sequence example above can define reading_t in the main program and again inside the interface body and still call show with it, and why it is the only way to repeat a type definition without a shared module. Otherwise, two copies of a definition are two types, even if they are identical, and a value of one cannot be assigned to or passed as the other:

! INTENTIONALLY NON-CONFORMING: two definitions with the same name are two different types.
module geometry_a
  implicit none
  type :: point_t
    real :: x, y
  end type point_t
end module geometry_a

module geometry_b
  implicit none
  type :: point_t                      ! same name, same components, but a different type
    real :: x, y
  end type point_t
end module geometry_b

program different_types
  use geometry_a, only: pa => point_t
  use geometry_b, only: pb => point_t
  implicit none
  type(pa) :: a
  type(pb) :: b
  a = pa(1.0, 2.0)
  b = a                                ! error: different derived types
  b = pb(a%x, a%y)                     ! fine: build the second type explicitly
  print *, b%x, b%y
end program different_types

GNU Fortran 16.2:

16_same_type_errors.f90:23:6:

   23 |   b = a                                ! error: different derived types
      |      1
Error: Cannot convert TYPE(point_t) to TYPE(point_t) at (1)

LLVM Flang 23:

error: Semantic errors in 16_same_type_errors.f90
16_same_type_errors.f90:23:3: error: No intrinsic or user-defined ASSIGNMENT(=) matches operand types TYPE(pb) and TYPE(pa)
    b = a                                ! error: different derived types
    ^^^^^

The usual cause in real code is a type copied into two modules, or a type defined in an include file that two program units each include without a shared module. The cure is to define the type in one module and use it from every unit that needs it; the renaming (pa => point_t) in the example is only there to give the two types different local names.

Derived types in input and output

A structure may be an item of a read, write or print list. For intrinsic formatted and list-directed I/O (12.6.3) it is treated as if its components appeared in the list in component order, so the format must supply a matching sequence of edit descriptors, and the components must be accessible where the statement is, and must not be pointers or allocatable. For unformatted I/O the structure is a single value in a processor-dependent form. Both restrictions disappear if the type has a defined input/output procedure, the dt edit descriptor, which is how grid_t is printed later in the page. This deliberately non-conforming program writes a structure with an allocatable component:

! INTENTIONALLY NON-CONFORMING: intrinsic I/O of a type with an allocatable component.
program io_allocatable
  implicit none
  type :: field_t
    integer :: n = 3
    real, allocatable :: u(:)
  end type field_t
  type(field_t) :: f
  f%u = [1.0, 2.0, 3.0]
  print *, f
end program io_allocatable

GNU Fortran 16.2:

13_io_allocatable.f90:10:12:

   10 |   print *, f
      |            1
Error: Data transfer element at (1) cannot have ALLOCATABLE components unless it is processed by a defined input/output procedure

LLVM Flang 23:

error: Semantic errors in 13_io_allocatable.f90
13_io_allocatable.f90:10:12: error: Derived type 'field_t' in I/O cannot have an allocatable or pointer direct component 'u' unless using defined I/O
    print *, f
             ^
13_io_allocatable.f90:6:26: Declaration of 'u'
      real, allocatable :: u(:)
                           ^

An allocatable or pointer ultimate component therefore needs a defined I/O procedure or an explicit write of each component. Namelist input of a structure follows the same rules and is described in Internal Files, Namelist and Non-Advancing I/O.

Derived types in heatflow

heatflow defines its configuration and its grid as derived types. config_t is a pure record of parameters, and every component has a default, so a bare declaration is a complete default configuration and a constructor with keywords changes only what it names. It is the type that the namelist reader fills in (Internal Files, Namelist and Non-Advancing I/O).

  type :: config_t
    integer(ip) :: nx = 21_ip
    integer(ip) :: ny = 21_ip
    real(dp) :: lx = 1.0_dp
    real(dp) :: ly = 1.0_dp
    real(dp) :: alpha = default_alpha
    real(dp) :: dt = 1.0e-4_dp
    real(dp) :: hot_temperature = 100.0_dp
    real(dp) :: boundary_value = 0.0_dp
    integer(ip) :: max_steps = default_max_steps
    real(dp) :: tolerance = default_tolerance
    integer(bc_kind) :: bc = dirichlet
    character(len=16) :: solver = 'ftcs'
    character(len=64) :: output = 'heatflow.csv'
  end type config_t

grid_t combines scalar components with an allocatable temperature array, so that assignment of a grid is a deep copy, and adds type-bound procedures and a defined formatted output, which are the subject of the following pages:

  type :: grid_t
    integer(ip) :: nx = 0_ip
    integer(ip) :: ny = 0_ip
    real(dp) :: dx = 0.0_dp
    real(dp) :: dy = 0.0_dp
    real(dp), allocatable :: u(:, :)
  contains
    procedure :: apply_boundary => grid_apply_boundary
    procedure :: set_hot_spot => grid_set_hot_spot
    procedure :: max_temperature => grid_max_temperature
    generic :: write(formatted) => write_formatted
    procedure, private :: write_formatted => grid_write_formatted
  end type grid_t

Both come from the sources in the project (src/heatflow_config.f90 and src/heatflow_grid.f90) and are compiled together with heatflow_kinds, heatflow_constants and heatflow_boundary. The driver below uses default initialization, a keyword constructor, a snapshot made by assignment, and the defined output of grid_t:

program heatflow_types
  use heatflow_kinds, only: dp
  use heatflow_boundary, only: neumann, boundary_name
  use heatflow_config, only: config_t
  use heatflow_grid, only: grid_t, make_grid
  implicit none
  type(config_t) :: cfg                     ! every component has a default: no constructor call needed
  type(config_t) :: custom
  type(grid_t) :: g, snapshot

  print '(a, 2i3, a, a)', 'default config: nx, ny =', cfg%nx, cfg%ny, '  bc=', boundary_name(cfg%bc)

  custom = config_t(nx=11, ny=11, bc=neumann, solver='jacobi')   ! keywords, the rest keep their defaults
  print '(a, 2i3, 2a)', 'custom config:  nx, ny =', custom%nx, custom%ny, '  bc=', boundary_name(custom%bc)
  print '(3a, f6.1)', 'solver=', trim(custom%solver), '  hot=', custom%hot_temperature

  g = make_grid(custom%nx, custom%ny, custom%lx, custom%ly)      ! grid_t has an allocatable component u
  call g%set_hot_spot(custom%hot_temperature)
  snapshot = g                                                    ! deep copy of u
  call g%apply_boundary(custom%bc, custom%boundary_value)
  g%u(6, 6) = 0.0_dp

  print *, g                                                      ! defined derived-type output of grid_t
  print *, snapshot
  print '(a, l1, a, i0, a, i0)', 'u allocated: ', allocated(snapshot%u), '  shape ', size(snapshot%u, 1), 'x', size(snapshot%u, 2)
end program heatflow_types

Output with GNU Fortran 16.2:

default config: nx, ny = 21 21  bc=dirichlet
custom config:  nx, ny = 11 11  bc=neumann
solver=jacobi  hot= 100.0
 grid_t(11x11) max= 0.000E+00
 grid_t(11x11) max= 1.000E+02
u allocated: T  shape 11x11

Flang prints the same lines, except that its list-directed output has no leading blank in front of the two grid_t lines. The snapshot kept the maximum of 100.0 of the hot spot while g, after apply_boundary and the explicit write to the centre cell, had a maximum of zero: the allocatable u was copied, not shared.

Runtime errors with derived types

Most mistakes with structures are mistakes about the status of a component. Two are shown with real output, because they behave differently on the two compilers.

An allocatable component that has not been allocated is unallocated, and referring to its elements is an error that GNU Fortran reports only with -fcheck=bounds (part of -fcheck=all):

program unallocated_component
  implicit none
  type :: field_t
    real, allocatable :: u(:)
  end type field_t
  type(field_t) :: f
  print *, 'about to read an unallocated component'
  print *, f%u(1)
end program unallocated_component

GNU Fortran 16.2 (backtrace frames omitted):

 about to read an unallocated component
At line 8 of file 12_unallocated_component.f90
Fortran runtime error: Index '1' of dimension 1 of array 'f%u' above upper bound of 0

LLVM Flang 23, without extra options:

Segmentation fault

A disassociated pointer component is worse, because the standard does not require any diagnosis and the behaviour is undefined:

program null_pointer_component
  implicit none
  type :: node_t
    integer :: value = 0
    type(node_t), pointer :: next => null()
  end type node_t
  type(node_t), target :: head
  type(node_t), pointer :: p
  print *, 'head%value =', head%value
  p => head%next
  print *, 'associated(p) =', associated(p)
  print *, 'p%value =', p%value
end program null_pointer_component

GNU Fortran 16.2 with -fcheck=all printed 0 for the value of the target of a disassociated pointer, a result that means nothing and could as well have been a crash; LLVM Flang 23 died with a segmentation fault:

 head%value =           0
 associated(p) = F
 p%value =           0
Segmentation fault

Test associated (p) before dereferencing a pointer component whose status is not certain, and prefer allocatable components, which have a status that allocated can always test.

Where the books are outdated

The books consulted for this section (listed in the bibliography) were written against older revisions of the language, and material derived from Fortran 90 and 95 courses is still common. For derived types the differences that matter are:

  • Allocatable components and recursive allocatable components. Older texts use pointer components for every dynamic or recursive component. Allocatable components (Fortran 2003, from TR 15581) are owned by the object, are deep-copied by assignment and are freed automatically; recursive allocatable components (Fortran 2008) extend this to lists and trees. The pointer form is now the exception, for sharing and for graphs.

  • Custom constructors. A generic interface with the name of the type (Fortran 2003) gives a type its own constructor while keeping the built-in one; older texts say that the structure constructor cannot be extended.

  • Omitted allocatable components. Since Fortran 2008 a structure constructor can leave out an allocatable component.

  • The authors' deprecations. The appendix of deprecated features in one of the books recommends avoiding sequence and storage association. That is the authors' recommendation, not the standard’s: sequence is not among the obsolescent features of J3/23-007r1 (B.3). Prefer bind(c) or a normal type unless storage association is really needed, and keep sequence for the cases above.

  • Non-standard records. Fortran 77 programs often model structures with parallel arrays or with the DEC structure and record extension. GNU Fortran still accepts the extension with -fdec-structure, "for compatibility only", and Flang lists the structure and record statements among its extensions (its union and map are not yet supported); derived types replace both.

  • No compiler information. The books say nothing about which compiler accepts what. The defect with recursive allocatable copies and the constructor defect above are examples of what a real compiler may still get wrong.

Standard version provenance and compiler support

Feature Introduced Compiler support checked on 2026-10-19

Derived types, structure constructors, %, sequence, private components, pointer components

Fortran 90

all compilers

Default initialization of components, => null()

Fortran 95

GNU Fortran 16.2 and LLVM Flang 23

Allocatable components (TR 15581), bind(c) types, procedure pointer components, type extension, type-bound procedures, generic interface named as the type (custom constructor)

Fortran 2003

both

Recursive allocatable components, omitting an allocatable component from a structure constructor

Fortran 2008

both accept the programs; the deep copy of a chain longer than two nodes is wrong in GNU Fortran 16.2

Component constructed from row%temperature into an allocatable component

Fortran 90 syntax, allocatable component Fortran 2003

valid; GNU Fortran 16.2 produces a descriptor that list-directed output rejects

DEC structure and record

extension (-fdec-structure)

not in the standard

The introduction editions follow the standard’s own history and the GNU Fortran status pages for Fortran 2003 and Fortran 2008; the lists of the Fortran 2023 additions in J3/23-007r1 do not touch the features of this page.

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.

Pitfalls

  • Forgetting that a derived type with a pointer component shares storage on assignment. b = a copies the pointer; both objects then own the same target, and one deallocate invalidates the other.

  • Assuming a value for every component. A component without a default is undefined in a new variable. Give every component a default, or build the value with a constructor that supplies all of them.

  • Believing a default initialization implies save. It does not (7.5.4.6); only explicit initialization in a declaration does.

  • Using intent(out) on a structure to keep part of its old value. intent(out) resets the defaulted components and leaves the rest undefined; use intent(inout).

  • A custom constructor that shadows the built-in one. If its signature matches the structure constructor, the function is called for every use, including those in the module itself.

  • A public constructor for a type whose components are private. Outside the module the built-in constructor is not usable, so a custom one is the only way to create values; export it with the type.

  • Writing the type name twice. Two copies of one definition are two different types; define the type once in a module.

  • Reading a private entity and getting an unrelated message. A private module variable shows up as "no IMPLICIT type", a private type name as "used before it is defined"; the cause in both cases is accessibility.

  • Printing a structure with an allocatable or pointer component with print *. It is rejected at compile time; write the components or provide a defined output procedure.

  • Comparing structures with == or comparing their real components exactly. There is no intrinsic ==, and an exact equality test on real components is almost never right.

  • Deep recursion in recursive allocatable chains. Copy and deallocation recurse once per node.

  • Trusting a deep copy of a long recursive chain on every compiler. GNU Fortran 16.2 copies only the first two levels.

  • Dereferencing a disassociated pointer component without checking. The result is undefined, not necessarily a crash.

  • Relying on the layout of an ordinary type. Only sequence and bind(c) types have a defined order, and neither removes padding.

See also Best Practices and Style, Common Gotchas and Pitfalls and, for the older alternatives to derived types, Legacy Features and Modernization.