Interfaces, Generics and Operator Overloading

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-18. Every example was compiled warning-free with gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all (GNU Fortran 16.2) and run, and also compiled and run with LLVM Flang 23; the output shown is real output. The one exception is the Fortran 2023 value feature, which GNU Fortran 16.2 rejects and which is therefore labelled as checked with Flang only. Examples that are deliberately wrong or that deliberately trigger a diagnostic are labelled as such. Interfaces are defined in clause 15 of the standard ("Procedures"): 15.4 covers the procedure interface, interface blocks, generic interfaces and procedure declaration statements, and 15.5.5 the resolution of generic references. Defined operations are in 10.1.6 and defined assignment in 10.2.1.4.

An interface is everything a caller needs to know about a procedure: whether it is a function or a subroutine, the names, types, ranks, attributes and intents of its dummy arguments, and the type of a function result. Fortran lets the compiler check a call against that description, lets one name stand for several procedures (generics), lets the language’s own operators and assignment work on your derived types (defined operators and defined assignment), and lets procedures be passed around as data (dummy procedures and procedure pointers). All of it builds on the procedures and modules pages, and the type-bound side is continued in Type-Bound Procedures, Abstract Types and Finalization and Object Oriented Programming. Compare the same ideas in C++ (operator overloading, templates), Rust (traits and operator overloading) and C (type-generic programming); the Fortran comparison page maps these concepts across languages.

Topic What it gives you Standard clause (J3/23-007r1)

Explicit interface

argument checking, keywords, assumed-shape, optional, array and pointer results

15.4.2

Interface block and interface body

declares the interface of an external, dummy or abstract procedure

15.4.3.2

generic statement

a generic name without an interface block (Fortran 2023)

15.4.3.3

Generic interface

one name for several specifics, resolved by the arguments

15.4.3.4.1, 15.5.5.2

Restrictions on generics

when two specifics are distinguishable

15.4.3.4.5

Defined operator

+, == or .name. applied to your own types

15.4.3.4.2, 10.1.6

Defined assignment

= between types that have no intrinsic assignment

15.4.3.4.3, 10.2.1.4

Type-bound generic

generic name, operator or assignment bound to a type

7.5.5

Abstract interface and procedure pointer

procedures as arguments, variables and components

15.4.1, 15.4.3.6, 10.2.2.4

Explicit and implicit interfaces

The interface of a procedure is either explicit or implicit (15.4.2.1). It is explicit for every internal procedure, module procedure and intrinsic procedure, for a subroutine or function within the scoping unit that defines it, and for a procedure declared by an interface body or by a procedure declaration with an interface. Everything else, in practice an external procedure with a bare external declaration or no declaration at all, has an implicit interface: the compiler knows nothing about its dummy arguments, takes the types of the actual arguments as they come, and cannot check anything. Statement functions always have an implicit interface.

Within the scope of a procedure the interface must be explicit when any of the following holds (15.4.2.2):

  • the call uses an argument keyword;

  • the procedure is used where a pure or simple procedure is required;

  • a dummy argument is allocatable, asynchronous, optional, pointer, target, value or volatile, is an assumed-shape array, is assumed-rank, is a coarray, is of a parameterized derived type or is polymorphic;

  • the result is an array, a pointer, allocatable, or has a nonconstant type parameter;

  • the procedure is elemental;

  • the procedure has the bind attribute.

Putting procedures in a module gives every caller that uses the module an explicit interface for free; this is why modern code has almost no hand-written interface blocks for its own procedures, and why implicit none (type, external) (see Variables, Declarations and Attributes) is a good default: its external part makes a call to a procedure without an explicit interface or an external declaration an error. The next listing is deliberately non-conforming: an external procedure with an implicit interface is called with argument keywords.

! NON-CONFORMING ON PURPOSE: an external procedure with an implicit interface
! cannot be called with argument keywords (15.4.2.2).
program implicit_keyword
  implicit none
  external :: smooth
  real :: u(3)
  u = [1.0, 2.0, 3.0]
  call smooth(n=3, u=u)
end program implicit_keyword

GNU Fortran 16.2 refuses it at compile time:

err_04_implicit_keyword.f90:8:16:

    8 |   call smooth(n=3, u=u)
      |                1
Error: Keyword argument requires explicit interface for procedure 'smooth' at (1)

Interface blocks and interface bodies

An interface block is delimited by interface and end interface and contains interface bodies (a copy of the procedure’s header, its dummy argument declarations and its end statement) and, in a generic block, procedure statements (15.4.3.2). The variants are named by what follows interface:

Form Name Purpose

interface

specific interface block

gives explicit interfaces to external or dummy procedures

abstract interface

abstract interface block

names a set of characteristics, with no procedure behind it

interface name

generic interface block

a generic name for the specifics it lists

interface operator(op)

generic interface block

a defined operator, op an intrinsic operator or .name.

interface assignment(=)

generic interface block

defined assignment

interface read(formatted) and the other three I/O forms

generic interface block

defined derived-type I/O (see Derived-Type and Asynchronous I/O)

An interface body needs the types it uses, but it does not see the host’s entities: a name must come from its own use or import statement (8.8), and import, none turns host association off completely. A body is only for procedures whose interface is not already explicit: an internal procedure, a module procedure and an intrinsic procedure never need one, because their interfaces are already explicit (15.4.3.1). The standard also lets the interface declare a non-pure procedure that is defined pure, but not the reverse.

The classic use is an external procedure written in another language or in another build target. The next program gives the BLAS dot product ddot an explicit interface, so that the call is checked and may use keywords. ddot is a Fortran procedure, so no bind(c) is needed; it is linked with -lblas and the actual name and meaning of each argument come from its documentation.

module heatflow_kinds
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: dp
  integer, parameter :: dp = real64
end module heatflow_kinds

module heatflow_blas
  use heatflow_kinds, only: dp
  implicit none
  private
  public :: ddot

  ! An interface body gives an external procedure an explicit interface.
  ! BLAS is written in Fortran, so no bind(c) is needed; the dummy argument
  ! names and the intents document the contract of the routine.
  interface
    function ddot(n, x, incx, y, incy) result(d)
      import :: dp
      integer, intent(in) :: n, incx, incy
      real(dp), intent(in) :: x(*), y(*)
      real(dp) :: d
    end function ddot
  end interface
end module heatflow_blas

program interface_body_demo
  use heatflow_kinds, only: dp
  use heatflow_blas, only: ddot
  implicit none
  real(dp) :: u(4), v(4)

  u = [1.0_dp, 2.0_dp, 3.0_dp, 4.0_dp]
  v = [4.0_dp, 3.0_dp, 2.0_dp, 1.0_dp]
  print '(a, f6.1)', 'ddot(u, v)        = ', ddot(size(u), u, 1, v, 1)
  print '(a, f6.1)', 'with keywords     = ', ddot(x=u, y=v, n=4, incx=1, incy=1)
end program interface_body_demo
ddot(u, v)        =   20.0
with keywords     =   20.0

The same interface bodies are also how a C library is described (bind(c), see Interoperability with C) and how the interface of a separate module procedure is declared in a module so that its body can live in a submodule (see Modules and Submodules).

Constraints worth remembering

  • An interface body must not describe a procedure that the same subprogram defines (C1501), and it must not contain a data, format, entry or statement function (C1506).

  • If end interface repeats the generic specification it has to match the opening one; == and .eq. count as the same (C1502).

  • A procedure must not have more than one explicit specific interface in a scoping unit; through use association it may only have more than one local name (15.4.3.2).

  • If the procedure does not exist in the program, the interface may be declared but the procedure must not be used.

  • An interface body of a pure procedure must give the intent of every dummy argument that is not a procedure, pointer or value argument (C1505).

Generic interfaces

A generic interface gives one name to several specific procedures. A caller writes the generic name and the compiler picks the specific whose arguments match. Fortran’s own intrinsics (abs, sqrt, sum) work this way, and so does every overloaded operator. A generic interface is always explicit (15.4.3.4.1).

There are two ways to write one. The generic interface block lists the specifics with module procedure (module procedures) or just procedure (any procedure with an explicit interface, including external and dummy procedures and procedure pointers: C1507 and C1508), or holds interface bodies. The generic statement, available from Fortran 2023 outside a type definition, does the same without an interface block and can carry an access specifier (15.4.3.3; WG5 N2212, section 5.23). The program below uses both: rms is an interface block with rank-1 and rank-2 real(dp) and rank-1 real(sp) specifics; mean_value is a generic statement with two specifics.

module heatflow_kinds
  use, intrinsic :: iso_fortran_env, only: real32, real64
  implicit none
  private
  public :: sp, dp
  integer, parameter :: sp = real32
  integer, parameter :: dp = real64
end module heatflow_kinds

module heatflow_stats
  use heatflow_kinds, only: sp, dp
  implicit none
  private
  public :: rms

  ! A generic interface block: the generic name is rms, the specifics keep
  ! their own names (rms_1d, rms_2d, rms_1d_sp) and stay callable.
  interface rms
    module procedure rms_1d
    module procedure rms_2d
    module procedure rms_1d_sp
  end interface rms

  ! The GENERIC statement (Fortran 2023) does the same without a block.
  generic, public :: mean_value => mean_1d, mean_2d

contains

  pure function rms_1d(x) result(r)
    real(dp), intent(in) :: x(:)
    real(dp) :: r
    r = sqrt(sum(x**2)/real(size(x), dp))
  end function rms_1d

  pure function rms_2d(x) result(r)
    real(dp), intent(in) :: x(:, :)
    real(dp) :: r
    r = sqrt(sum(x**2)/real(size(x), dp))
  end function rms_2d

  pure function rms_1d_sp(x) result(r)
    real(sp), intent(in) :: x(:)
    real(sp) :: r
    r = sqrt(sum(x**2)/real(size(x), sp))
  end function rms_1d_sp

  pure function mean_1d(x) result(r)
    real(dp), intent(in) :: x(:)
    real(dp) :: r
    r = sum(x)/real(size(x), dp)
  end function mean_1d

  pure function mean_2d(x) result(r)
    real(dp), intent(in) :: x(:, :)
    real(dp) :: r
    r = sum(x)/real(size(x), dp)
  end function mean_2d

end module heatflow_stats

program generic_rms_demo
  use heatflow_kinds, only: sp, dp
  use heatflow_stats, only: rms, mean_value
  implicit none
  real(dp) :: row(4), plate(3, 3)
  real(sp) :: row_sp(4)
  integer :: k

  row = [1.0_dp, 2.0_dp, 3.0_dp, 4.0_dp]
  plate = reshape([(real(k, dp), k=1, 9)], [3, 3])
  row_sp = real(row, sp)

  print '(a, f8.4)', 'rms of a rank-1 dp array : ', rms(row)
  print '(a, f8.4)', 'rms of a rank-2 dp array : ', rms(plate)
  print '(a, f8.4)', 'rms of a rank-1 sp array : ', rms(row_sp)
  print '(a, f8.4)', 'mean of a rank-1 dp array: ', mean_value(row)
  print '(a, f8.4)', 'mean of a rank-2 dp array: ', mean_value(plate)
end program generic_rms_demo
rms of a rank-1 dp array :   2.7386
rms of a rank-2 dp array :   5.6273
rms of a rank-1 sp array :   2.7386
mean of a rank-1 dp array:   2.5000
mean of a rank-2 dp array:   5.0000

Things to remember about the names involved:

  • The specific names stay usable: rms_1d is still accessible inside the module and would be outside it if it were public. Put the specifics private and export only the generic name when the specifics are an implementation detail.

  • A generic name may equal one of its own specifics, or any other accessible generic name. In the second case the two sets are merged (15.4.3.4.1), which is how a program extends a library’s generic.

  • A specific procedure may not be listed twice under the same generic identifier (C1509, C1510).

  • A generic name may equal a derived-type name, in which case every specific must be a function; this is how a type gets a second structure constructor next to the default one (the flux_t example below). The arguments of a default structure constructor must not also be a valid generic reference (C7108, 7.5.10), so the generic takes precedence where both would fit.

  • A generic name that equals the generic name of an intrinsic procedure extends the intrinsic: a reference consistent with both uses the specific from your interface, and other references still reach the intrinsic. Only if your specifics and the intrinsic are not all functions or not all subroutines is the intrinsic hidden under that name (15.4.3.4.5).

  • A function argument of a specific must have its type and type parameters declared explicitly in the specific’s interface.

Extending a generic across modules

The same generic name in a second interface block of another module adds specifics to the one accessed by use. The merged generic is what a caller of the second module sees. This is the standard technique for adding support for a new type to a library’s generic without editing the library.

module heatflow_describe_base
  implicit none
  private
  public :: describe, describe_int

  interface describe
    module procedure describe_int
    module procedure describe_logical
  end interface describe

contains

  subroutine describe_int(n)
    integer, intent(in) :: n
    print '(a, i0)', 'integer: ', n
  end subroutine describe_int

  subroutine describe_logical(flag)
    logical, intent(in) :: flag
    print '(a, l1)', 'logical: ', flag
  end subroutine describe_logical

end module heatflow_describe_base

module heatflow_describe_extra
  use heatflow_describe_base, only: describe
  implicit none
  private
  public :: describe

  ! The same generic name in a second interface block extends the generic
  ! that was accessed by use association: describe now has three specifics.
  interface describe
    module procedure describe_text
  end interface describe

contains

  subroutine describe_text(text)
    character(len=*), intent(in) :: text
    print '(a, a)', 'text   : ', text
  end subroutine describe_text

end module heatflow_describe_extra

program generic_extension_demo
  use heatflow_describe_extra, only: describe
  use heatflow_describe_base, only: describe_int
  implicit none

  call describe(42)
  call describe(.true.)
  call describe('boundary')
  ! A specific procedure can still be called by its own name when it is
  ! accessible under that name.
  call describe_int(7)
end program generic_extension_demo
integer: 42
logical: T
text   : boundary
integer: 7

The only: clause on the second use shows that a specific accessible under its own name is a separate access path from the one under the generic name: the program imports describe_int explicitly, so it can call it directly.

Distinguishing specific procedures

Two specifics under one generic name must be distinguishable, otherwise some call could match both. The compiler checks this when the generic is declared (15.4.3.4.5), long before any call. The rules use two definitions.

  • A dummy argument is TKR compatible with another if it is type compatible, has the same kind type parameters and has the same rank (or either is assumed-rank).

  • Two dummy arguments are distinguishable if one is a procedure and the other a data object; or both are data objects (or known to be functions) and neither is TKR compatible with the other; or one is allocatable and the other pointer without intent(in); or one is a function with nonzero rank and the other is not known to be a function.

C1514 then says that two procedures with the same generic name must both be subroutines or both be functions and satisfy at least one of:

  1. a non-passed-object dummy data object exists in one of them for which the number of nonoptional dummy data objects in that procedure that are TKR compatible with it exceeds the number of dummy data objects in the other, optional or not, that are not distinguishable from it;

  2. one has more nonoptional dummy procedures than the other has dummy procedures (added in Fortran 2018: WG5 N2161, section 5.7);

  3. both have passed-object dummy arguments and those are distinguishable;

  4. one has a nonoptional dummy argument that is distinguishable by position (taking the passed-object argument out of the count) and a nonoptional dummy argument that is distinguishable by name, where the positional one is the same argument as, or earlier than, the named one.

The fourth rule is the one that guarantees a keyword call is unambiguous as well as a positional one. The rules say "kind type parameters", so real(sp) and real(dp) are distinguishable, and "rank", so a vector and a matrix are. They say nothing of the names alone, of the intent, or of the result type: those cannot distinguish specifics.

The program below combines the cases that do work. The two scalar specifics are distinguished by the type of their second argument (real versus integer), so adjust(x, factor=3.0_dp) and adjust(x, passes=2) are both clear. A rank-1 specific and an elemental specific coexist: the rank-1 one is chosen for a vector, and the elemental one for everything else.

module heatflow_kinds
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: dp
  integer, parameter :: dp = real64
end module heatflow_kinds

module heatflow_adjust
  use heatflow_kinds, only: dp
  implicit none
  private
  public :: adjust

  interface adjust
    module procedure adjust_by_factor
    module procedure adjust_by_passes
    module procedure adjust_vector
    module procedure adjust_elemental
  end interface adjust

contains

  ! The second argument differs in TYPE (real versus integer), so the two
  ! specifics are distinguishable by position and by keyword.
  pure function adjust_by_factor(t, factor) result(r)
    real(dp), intent(in) :: t
    real(dp), intent(in) :: factor
    real(dp) :: r
    r = t*factor
  end function adjust_by_factor

  pure function adjust_by_passes(t, passes) result(r)
    real(dp), intent(in) :: t
    integer, intent(in) :: passes
    real(dp) :: r
    r = t*0.5_dp**passes
  end function adjust_by_passes

  ! A rank-1 specific: preferred over the elemental one for rank-1 actuals.
  pure function adjust_vector(t) result(r)
    real(dp), intent(in) :: t(:)
    real(dp) :: r(size(t))
    r = t - sum(t)/real(size(t), dp)
  end function adjust_vector

  ! An elemental specific: used for every other rank.
  elemental function adjust_elemental(t) result(r)
    real(dp), intent(in) :: t
    real(dp) :: r
    r = max(t, 0.0_dp)
  end function adjust_elemental

end module heatflow_adjust

program resolution_demo
  use heatflow_kinds, only: dp
  use heatflow_adjust, only: adjust
  implicit none
  real(dp) :: line(3), plate(2, 2)

  line = [10.0_dp, 20.0_dp, 60.0_dp]
  plate = reshape([-1.0_dp, 2.0_dp, -3.0_dp, 4.0_dp], [2, 2])

  print '(a, f6.1)', 'factor= : ', adjust(10.0_dp, factor=3.0_dp)
  print '(a, f6.1)', 'passes= : ', adjust(10.0_dp, passes=2)
  print '(a, f6.1)', 'by type : ', adjust(8.0_dp, 3)
  print '(a, 3f7.1)', 'rank 1  : ', adjust(line)
  print '(a, 4f7.1)', 'rank 2  : ', adjust(plate)
  print '(a, f6.1)', 'scalar  : ', adjust(-5.0_dp)
end program resolution_demo
factor= :   30.0
passes= :    2.5
by type :    1.0
rank 1  :   -20.0  -10.0   30.0
rank 2  :     0.0    2.0    0.0    4.0
scalar  :    0.0

Ambiguous generics are rejected at the declaration

The next three listings are all deliberately non-conforming and show what the compiler reports for them. The first has two specifics that differ only by the name of the second dummy argument. The names are not enough: the argument types and ranks are identical, so there is no distinguishable argument by position at all.

! NON-CONFORMING ON PURPOSE: the two specifics differ only in the name of
! their second dummy argument, which does not make them distinguishable.
module bad_adjust
  implicit none
  interface adjust
    module procedure adjust_by_factor
    module procedure adjust_by_offset
  end interface adjust
contains
  pure function adjust_by_factor(t, factor) result(r)
    real, intent(in) :: t, factor
    real :: r
    r = t*factor
  end function adjust_by_factor
  pure function adjust_by_offset(t, offset) result(r)
    real, intent(in) :: t, offset
    real :: r
    r = t + offset
  end function adjust_by_offset
end module bad_adjust
err_01_name_only.f90:10:2:

   10 |   pure function adjust_by_factor(t, factor) result(r)
      |  1
......
   15 |   pure function adjust_by_offset(t, offset) result(r)
      |  2
Error: Ambiguous interfaces in generic interface 'adjust' for 'adjust_by_factor' at (1) and 'adjust_by_offset' at (2)

The second makes an argument optional. The call amplify(1.0) would be a valid call of both, so they are not distinguishable. Optional arguments are the usual source of this error in real code: a nonoptional argument has to be the distinguishing one.

! NON-CONFORMING ON PURPOSE: a call such as amplify(1.0) would match both
! specifics, because the second dummy argument of amplify_two is optional.
module bad_amplify
  implicit none
  interface amplify
    module procedure amplify_one
    module procedure amplify_two
  end interface amplify
contains
  function amplify_one(a) result(r)
    real, intent(in) :: a
    real :: r
    r = a
  end function amplify_one
  function amplify_two(a, b) result(r)
    real, intent(in) :: a
    real, intent(in), optional :: b
    real :: r
    r = a
    if (present(b)) r = a*b
  end function amplify_two
end module bad_amplify
err_02_optional.f90:10:2:

   10 |   function amplify_one(a) result(r)
      |  1
......
   15 |   function amplify_two(a, b) result(r)
      |  2
Error: Ambiguous interfaces in generic interface 'amplify' for 'amplify_one' at (1) and 'amplify_two' at (2)

The third is distinguishable positionally (a real first against an integer first) but not by keyword, because the names t and n are the same in both and a call blend(t=1.0, n=2) fits either. It is why the fourth rule needs a distinguishing argument by position and one by name.

! NON-CONFORMING ON PURPOSE: the specifics are distinguishable by position
! (real first versus integer first) but not by keyword, so a call such as
! blend(t=1.0, n=2) would match both.
module bad_blend
  implicit none
  interface blend
    module procedure blend_tn
    module procedure blend_nt
  end interface blend
contains
  function blend_tn(t, n) result(r)
    real, intent(in) :: t
    integer, intent(in) :: n
    real :: r
    r = t*n
  end function blend_tn
  function blend_nt(n, t) result(r)
    integer, intent(in) :: n
    real, intent(in) :: t
    real :: r
    r = t + n
  end function blend_nt
end module bad_blend
err_06_keyword_ambiguity.f90:11:2:

   11 |   function blend_tn(t, n) result(r)
      |  1
......
   17 |   function blend_nt(n, t) result(r)
      |  2
Error: Ambiguous interfaces in generic interface 'blend' for 'blend_tn' at (1) and 'blend_nt' at (2)

Generic resolution

A reference name(args) to a name established as generic is resolved at compile time, in this order (15.5.5.2):

flowchart TD A["Reference to a generic name"] --> B{"Consistent with a nonelemental specific
defined here or accessed by use?"} B -- yes --> R1["That specific"] B -- no --> C{"Consistent with an elemental specific?"} C -- yes --> R2["That elemental specific"] C -- no --> D{"Name has the INTRINSIC attribute here
and the reference matches the intrinsic?"} D -- yes --> R3["That intrinsic procedure"] D -- no --> E{"Name generic in the host scoping unit?"} E -- yes --> F["Repeat these rules in the host"] E -- no --> G{"Name is an intrinsic procedure
and the reference matches?"} G -- yes --> R3 G -- no --> H["Compile-time error"]

Because the nonelemental search happens first, a rank-specific specific always beats an elemental one: adjust(line) above takes the rank-1 specific, adjust(plate) falls through to the elemental one, and a scalar goes to the elemental as well. Nothing is decided at run time; there is no virtual dispatch for a plain generic. (Type-bound generics resolve on the declared type at compile time and the specific binding on the dynamic type at run time; that is covered in Type-Bound Procedures, Abstract Types and Finalization.)

Generic resolution is also how the language does rank-independent procedures without macros or templates, by writing one specific per rank or an elemental one; the Generic and Rank Independent Programming page shows the other techniques, including the Fortran 2028 preview of real templates.

Defined operators

A generic interface whose name is operator(op) defines what an operator does for operands of your types. op is either an intrinsic operator (+, -, *, /, **, //, ==, /=, <, <=, >, >=, .eq., .ne., .lt., .le., .gt., .ge., .not., .and., .or., .eqv., .neqv.) or a defined operator .name. made of letters between two dots. The rules (15.4.3.4.2):

  • every specific is a function with one argument (a prefix operation) or two (an infix operation); none with zero or more than two;

  • every dummy argument is a nonoptional data object with intent(in) or, since Fortran 2023, value;

  • the function result must not have assumed character length;

  • for an intrinsic operator the argument count must match the intrinsic uses of the operator, and the types, kinds or ranks must differ from those the intrinsic operator already handles, so real + real cannot be redefined;

  • + and - are both unary and binary, and one generic interface may hold both; the argument count tells them apart;

  • extending one of the two forms of a relational operator extends both: <= and .le. are the same operator, and the same goes for the other five pairs.

Precedence is the precedence of the operator’s category, whatever the function does (10.1.3): a defined unary operator binds tighter than every intrinsic operator, and a defined binary operator binds looser than all of them, including .eqv.. That is why a user-defined .dot. needs parentheses around anything but simple operands, and why a unary .norm. applies before an addition.

Operator category Precedence Operators

extension (defined unary)

highest

.name. used as a prefix

numeric

next

**, then * /, then unary + -, then binary + -

character

next

//

relational

next

== /= < <= > >= and the dotted forms

logical

next

.not., .and., .or., then .eqv. .neqv.

extension (defined binary)

lowest

.name. used between two operands

The next module overloads the operators for a small flux_t type, the heat flux through one cell face of the heatflow grid. It defines + (binary), - (unary and binary in the same interface), * in both operand orders, ==, a new binary operator .dot., a new unary operator .norm., and a generic constructor named flux_t. Defined operators cannot select a component of their result, so the demo uses a show helper.

module heatflow_kinds
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: dp
  integer, parameter :: dp = real64
end module heatflow_kinds

module heatflow_flux
  use heatflow_kinds, only: dp
  implicit none
  private
  public :: flux_t, operator(+), operator(-), operator(*), operator(==)
  public :: operator(.dot.), operator(.norm.)

  real(dp), parameter :: tolerance = 1.0e-12_dp

  ! Heat flux through one cell face: a two-component vector.
  type :: flux_t
    real(dp) :: qx = 0.0_dp
    real(dp) :: qy = 0.0_dp
  end type flux_t

  ! A generic name equal to the type name overrides nothing: the default
  ! structure constructor stays available and this adds a second one.
  interface flux_t
    module procedure flux_from_array
  end interface flux_t

  interface operator(+)           ! binary +
    module procedure flux_add
  end interface operator(+)

  interface operator(-)           ! unary and binary -: both are generic
    module procedure flux_negate
    module procedure flux_subtract
  end interface operator(-)

  interface operator(*)           ! scalar * flux and flux * scalar
    module procedure real_times_flux
    module procedure flux_times_real
  end interface operator(*)

  interface operator(==)
    module procedure flux_equal
  end interface operator(==)

  interface operator(.dot.)       ! a new binary operator
    module procedure flux_dot
  end interface operator(.dot.)

  interface operator(.norm.)      ! a new unary operator
    module procedure flux_norm
  end interface operator(.norm.)

contains

  pure function flux_from_array(q) result(f)
    real(dp), intent(in) :: q(2)
    type(flux_t) :: f
    f%qx = q(1)
    f%qy = q(2)
  end function flux_from_array

  pure function flux_add(a, b) result(f)
    type(flux_t), intent(in) :: a, b
    type(flux_t) :: f
    f = flux_t(a%qx + b%qx, a%qy + b%qy)
  end function flux_add

  pure function flux_negate(a) result(f)
    type(flux_t), intent(in) :: a
    type(flux_t) :: f
    f = flux_t(-a%qx, -a%qy)
  end function flux_negate

  pure function flux_subtract(a, b) result(f)
    type(flux_t), intent(in) :: a, b
    type(flux_t) :: f
    f = flux_t(a%qx - b%qx, a%qy - b%qy)
  end function flux_subtract

  pure function real_times_flux(s, a) result(f)
    real(dp), intent(in) :: s
    type(flux_t), intent(in) :: a
    type(flux_t) :: f
    f = flux_t(s*a%qx, s*a%qy)
  end function real_times_flux

  pure function flux_times_real(a, s) result(f)
    type(flux_t), intent(in) :: a
    real(dp), intent(in) :: s
    type(flux_t) :: f
    f = flux_t(s*a%qx, s*a%qy)
  end function flux_times_real

  pure function flux_equal(a, b) result(same)
    type(flux_t), intent(in) :: a, b
    logical :: same
    same = abs(a%qx - b%qx) <= tolerance .and. abs(a%qy - b%qy) <= tolerance
  end function flux_equal

  pure function flux_dot(a, b) result(d)
    type(flux_t), intent(in) :: a, b
    real(dp) :: d
    d = a%qx*b%qx + a%qy*b%qy
  end function flux_dot

  pure function flux_norm(a) result(n)
    type(flux_t), intent(in) :: a
    real(dp) :: n
    n = sqrt(a%qx**2 + a%qy**2)
  end function flux_norm

end module heatflow_flux

program flux_operators_demo
  use heatflow_kinds, only: dp
  use heatflow_flux
  implicit none
  type(flux_t) :: a, b

  a = flux_t(3.0_dp, 4.0_dp)          ! default structure constructor
  b = flux_t([1.0_dp, 2.0_dp])        ! the generic constructor

  call show('a + b          ', a + b)
  call show('-a             ', -a)
  call show('a - b          ', a - b)
  call show('2 * a          ', 2.0_dp*a)
  call show('a * 0.5        ', a*0.5_dp)
  print '(a, f6.1)', 'a .dot. b      = ', a .dot. b
  print '(a, f6.1)', '.norm. a       = ', .norm. a
  print '(a, l2)', 'a == a         = ', a == a
  print '(a, l2)', 'a .eq. b       = ', a .eq. b      ! the same operator
  ! Precedence is that of the operator's category (10.1.3): a defined unary
  ! operator binds tighter than +, a defined binary operator looser than *.
  print '(a, f6.1)', '.norm. a + 1   = ', .norm. a + 1.0_dp
  print '(a, f6.1)', 'a .dot. 2 * b  = ', a .dot. 2.0_dp*b

contains

  subroutine show(label, f)
    character(len=*), intent(in) :: label
    type(flux_t), intent(in) :: f
    print '(a, a, 2f6.1)', label, '= ', f%qx, f%qy
  end subroutine show

end program flux_operators_demo
a + b          =    4.0   6.0
-a             =   -3.0  -4.0
a - b          =    2.0   2.0
2 * a          =    6.0   8.0
a * 0.5        =    1.5   2.0
a .dot. b      =   11.0
.norm. a       =    5.0
a == a         =  T
a .eq. b       =  F
.norm. a + 1   =    6.0
a .dot. 2 * b  =   22.0

Points to take from this run:

  • a .eq. b and a == b call the same function, because only operator(==) was written (C1502, 10.1.6.2).

  • .norm. a + 1 is (.norm. a) + 1, giving 6.0; if .norm. bound looser it would try a + 1, which has no definition. a .dot. 2 * b is a .dot. (2 * b) because * binds tighter than the defined binary operator; it gives 22.0, the same as 2 * (a .dot. b) only because the dot product is linear.

  • flux_t(3.0_dp, 4.0_dp) still calls the default structure constructor, and flux_t([1.0_dp, 2.0_dp]) the generic one: they are distinguished by the number and rank of the arguments.

  • Exact equality on floating-point components is rarely what the caller means. The == above compares within a tolerance, which also keeps -Wcompare-reals quiet; define the tolerance in the module, document it, and do not expect == to be transitive.

  • A defined operation is evaluated as a function reference, so every rule for actual arguments applies to the operands, and the compiler may evaluate an equivalent expression instead as long as parentheses are respected (10.1.6.3).

Elemental operators and intrinsic names

An elemental specific works on operands of any rank, so one definition of + serves a scalar, a vector and a matrix of flux_t (a scalar operand is broadcast). The same module extends the intrinsic generic abs to flux_t; the intrinsic abs of a real still works, because a reference that matches no specific falls through to the intrinsic (15.5.5.2).

module heatflow_kinds
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: dp
  integer, parameter :: dp = real64
end module heatflow_kinds

module heatflow_flux_elemental
  use heatflow_kinds, only: dp
  implicit none
  private
  public :: flux_t, operator(+), abs

  type :: flux_t
    real(dp) :: qx = 0.0_dp
    real(dp) :: qy = 0.0_dp
  end type flux_t

  ! One elemental specific serves scalars and arrays of every rank.
  interface operator(+)
    module procedure flux_add
  end interface operator(+)

  ! Extending an intrinsic generic name: abs(flux) is now the magnitude,
  ! and abs of the intrinsic types keeps working.
  interface abs
    module procedure flux_abs
  end interface abs

contains

  elemental function flux_add(a, b) result(f)
    type(flux_t), intent(in) :: a, b
    type(flux_t) :: f
    f = flux_t(a%qx + b%qx, a%qy + b%qy)
  end function flux_add

  elemental function flux_abs(a) result(m)
    type(flux_t), intent(in) :: a
    real(dp) :: m
    m = sqrt(a%qx**2 + a%qy**2)
  end function flux_abs

end module heatflow_flux_elemental

program elemental_operator_demo
  use heatflow_kinds, only: dp
  use heatflow_flux_elemental, only: flux_t, operator(+), abs
  implicit none
  type(flux_t) :: row(3), shifted(3), grid(2, 2)

  row = [flux_t(3.0_dp, 4.0_dp), flux_t(6.0_dp, 8.0_dp), flux_t(0.0_dp, 1.0_dp)]
  shifted = row + flux_t(0.0_dp, 0.0_dp)       ! array + scalar
  grid = reshape(row([1, 2, 3, 1]), [2, 2])
  grid = grid + grid                           ! rank-2 + rank-2

  print '(a, 3f6.1)', 'abs(row)       : ', abs(row)
  print '(a, 3f6.1)', 'abs(shifted)   : ', abs(shifted)
  print '(a, 4f6.1)', 'abs(grid+grid) : ', abs(grid)
  print '(a, f6.1)', 'abs(-2.5)      : ', abs(-2.5_dp)
end program elemental_operator_demo
abs(row)       :    5.0  10.0   1.0
abs(shifted)   :    5.0  10.0   1.0
abs(grid+grid) :   10.0  20.0   2.0  10.0
abs(-2.5)      :    2.5

The value attribute in operators (Fortran 2023)

Fortran 2023 allows value instead of intent(in) for an operand of a defined operation or assignment (15.4.3.4.2; WG5 N2212, section 5.24). LLVM Flang 23 accepts it, while GNU Fortran 16.2 still requires intent(in) and reports an error. The listing below is therefore checked with Flang only.

module heatflow_value_ops
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: flux_t, operator(*)

  type :: flux_t
    real(real64) :: qx = 0.0_real64
    real(real64) :: qy = 0.0_real64
  end type flux_t

  interface operator(*)
    module procedure scale_flux
  end interface operator(*)

contains

  ! Fortran 2023: a dummy argument of a defined operation or assignment may
  ! have the VALUE attribute instead of INTENT(IN).
  pure function scale_flux(s, f) result(g)
    real(real64), value :: s
    type(flux_t), value :: f
    type(flux_t) :: g
    g = flux_t(s*f%qx, s*f%qy)
  end function scale_flux

end module heatflow_value_ops

program value_operator_demo
  use, intrinsic :: iso_fortran_env, only: real64
  use heatflow_value_ops, only: flux_t, operator(*)
  implicit none
  type(flux_t) :: g
  g = 3.0_real64*flux_t(1.0_real64, 2.0_real64)
  print '(a, 2f6.1)', '3 * flux = ', g%qx, g%qy
end program value_operator_demo

LLVM Flang 23 compiles and runs it:

3 * flux =    3.0   6.0

GNU Fortran 16.2 stops at the interface with this first message (the rest is a consequence of the missing module file):

11_value_in_operators.f90:20:2:

   20 |   pure function scale_flux(s, f) result(g)
      |  1
Error: First argument of operator interface at (1) must be INTENT(IN)
11_value_in_operators.f90:31:7:

Operator rules that the compiler checks

The last operator listing is deliberately non-conforming: a dummy argument that is not intent(in), a function with three arguments used as an operator, and a defined assignment between two default reals (which is intrinsic and cannot be redefined).

! NON-CONFORMING ON PURPOSE: three ways to break the rules for defined
! operators and defined assignment (15.4.3.4.2 and 15.4.3.4.3).
module bad_rules
  implicit none
  type :: flux_t
    real :: qx = 0.0, qy = 0.0
  end type flux_t
  interface operator(+)
    module procedure add_inout                  ! dummy argument not intent(in)
  end interface operator(+)
  interface operator(*)
    module procedure blend_three                ! operators take one or two arguments
  end interface operator(*)
  interface assignment(=)
    module procedure real_from_real             ! both sides are default real
  end interface assignment(=)
contains
  function add_inout(a, b) result(f)
    type(flux_t), intent(inout) :: a
    type(flux_t), intent(in) :: b
    type(flux_t) :: f
    f = flux_t(a%qx + b%qx, a%qy + b%qy)
  end function add_inout
  function blend_three(a, b, c) result(f)
    type(flux_t), intent(in) :: a, b, c
    type(flux_t) :: f
    f = flux_t(a%qx + b%qx + c%qx, a%qy + b%qy + c%qy)
  end function blend_three
  subroutine real_from_real(x, y)
    real, intent(out) :: x
    real, intent(in) :: y
    x = y
  end subroutine real_from_real
end module bad_rules
err_03_operator_rules.f90:18:2:

   18 |   function add_inout(a, b) result(f)
      |  1
Error: First argument of operator interface at (1) must be INTENT(IN)
err_03_operator_rules.f90:24:2:

   24 |   function blend_three(a, b, c) result(f)
      |  1
Error: Operator interface at (1) has the wrong number of arguments
err_03_operator_rules.f90:29:3:

   29 |   subroutine real_from_real(x, y)
      |   1~~~~~~~~~~~~~~~~~~~~~~~~
Error: Assignment operator interface at (1) must not redefine an INTRINSIC type assignment

Defined assignment

Intrinsic assignment copies every component, which is what you want for plain data, but not for a type whose components are pointers or that must convert between representations. A generic interface named assignment(=) supplies the conversions (15.4.3.4.3). Each specific is a subroutine with exactly two nonoptional data-object dummy arguments: the first has intent(out) or intent(inout), the second intent(in) or value. The two arguments must differ in a way intrinsic assignment does not already cover: the second is an array of a different rank, the types do not conform under Table 10.8 (the table of intrinsic assignment type combinations), or the first is of derived type. a = b becomes call sub(a, (b)), with the right-hand side wrapped in parentheses so that it is an expression.

Consequences of that translation:

  • A defined assignment is an ordinary subroutine call: intent(out) finalizes and default-initializes the left side first, and the right side cannot be modified.

  • Because the right-hand side is an expression in parentheses, a second dummy argument with the allocatable or pointer attribute can never be associated by defined assignment; call the subroutine directly in that case (NOTE 2 of 15.4.3.4.3).

  • An elemental defined assignment applies element by element to arrays, and a scalar right side is broadcast (10.2.1.5). That makes cells = 0.5_dp below fill a whole array.

  • A defined assignment is used only for the type combinations it covers. Inside an intrinsic assignment of a derived type, a nonpointer nonallocatable component whose type has a consistent type-bound defined assignment is assigned with it, and the other components by intrinsic assignment (10.2.1.3); a module-level interface assignment(=) is not applied to components.

Type-bound generics

The same interfaces can be bound to a type with generic in the type-bound procedure part (7.5.5). The statement is generic :: operator(+) => add, generic :: assignment(=) => from_array, from_scalar or generic :: name => specific_1, specific_2. For a non-name generic-spec (an operator or assignment) every specific binding must have a passed-object dummy argument (C779), because the binding is found through the declared type of one of the operands (10.1.6.1). The passed-object dummy argument is the first one unless pass(arg) says otherwise, and nopass removes it (7.5.4.5). C765 requires it to be a scalar, nonpointer, nonallocatable class(t) dummy argument for an extensible type t.

Differences from a module-level generic interface:

  • a type-bound generic name must not equal one of the type’s specific binding names (NOTE 1 of 7.5.5), unlike an ordinary generic name;

  • pass(b) is how a binding gets the object as the second operand, as real_times does for 2.0_dp*a below;

  • the generic bindings are inherited by an extension, and a generic statement with the same generic-spec in the extension adds specifics to the inherited generic (7.5.5, C778); an extension overrides a specific binding by redeclaring the binding name;

  • a type-bound defined operator is visible to anyone who has an object of the type, with no use of an operator name needed.

The listing binds +, * (with pass and pass(b)) and three forms of = to flux_t: from an array, from a scalar (elemental, so an array of flux_t can be filled in one statement), and to an array (the object is the second argument, pass(f)). A pure procedure cannot have a polymorphic intent(out) dummy argument, so the passed-object left side is intent(inout) in the pure and elemental specifics.

module heatflow_kinds
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: dp
  integer, parameter :: dp = real64
end module heatflow_kinds

module heatflow_flux_tbp
  use heatflow_kinds, only: dp
  implicit none
  private
  public :: flux_t

  type :: flux_t
    real(dp) :: qx = 0.0_dp
    real(dp) :: qy = 0.0_dp
  contains
    procedure, private :: add
    procedure, private :: times_real
    procedure, pass(b), private :: real_times
    procedure, private :: from_array
    procedure, private :: from_scalar
    procedure, pass(f), private :: to_array
    ! A type-bound generic needs a passed-object dummy argument for every
    ! operator or assignment binding (C779); pass(b) and pass(f) say which.
    generic :: operator(+) => add
    generic :: operator(*) => times_real, real_times
    generic :: assignment(=) => from_array, from_scalar, to_array
  end type flux_t

contains

  pure function add(a, b) result(f)
    class(flux_t), intent(in) :: a
    type(flux_t), intent(in) :: b
    type(flux_t) :: f
    f = flux_t(a%qx + b%qx, a%qy + b%qy)
  end function add

  pure function times_real(a, s) result(f)
    class(flux_t), intent(in) :: a
    real(dp), intent(in) :: s
    type(flux_t) :: f
    f = flux_t(s*a%qx, s*a%qy)
  end function times_real

  pure function real_times(s, b) result(f)
    real(dp), intent(in) :: s
    class(flux_t), intent(in) :: b
    type(flux_t) :: f
    f = flux_t(s*b%qx, s*b%qy)
  end function real_times

  ! Defined assignment: the first argument is intent(out) or intent(inout),
  ! the second intent(in) or value; the right-hand side is passed in
  ! parentheses. A pure procedure cannot have a polymorphic intent(out)
  ! dummy argument, so the passed-object lhs is intent(inout) here.
  elemental subroutine from_scalar(lhs, rhs)
    class(flux_t), intent(inout) :: lhs
    real(dp), intent(in) :: rhs
    lhs%qx = rhs
    lhs%qy = rhs
  end subroutine from_scalar

  pure subroutine from_array(lhs, rhs)
    class(flux_t), intent(inout) :: lhs
    real(dp), intent(in) :: rhs(2)
    lhs%qx = rhs(1)
    lhs%qy = rhs(2)
  end subroutine from_array

  pure subroutine to_array(lhs, f)
    real(dp), intent(out) :: lhs(2)
    class(flux_t), intent(in) :: f
    lhs = [f%qx, f%qy]
  end subroutine to_array

end module heatflow_flux_tbp

program type_bound_demo
  use heatflow_kinds, only: dp
  use heatflow_flux_tbp, only: flux_t
  implicit none
  type(flux_t) :: a, b, cells(3)
  real(dp) :: raw(2)

  a = [3.0_dp, 4.0_dp]          ! defined assignment, array on the right
  b = 1.5_dp                    ! defined assignment, scalar on the right
  raw = a + b                   ! operator(+), then defined assignment to an array
  print '(a, 2f6.1)', 'raw   = a + b   : ', raw

  raw = 2.0_dp*a                ! operator(*) bound with pass(b)
  print '(a, 2f6.1)', 'raw   = 2 * a   : ', raw
  raw = a*0.5_dp                ! operator(*) bound with the default pass
  print '(a, 2f6.1)', 'raw   = a * 0.5 : ', raw

  cells = 0.5_dp               ! the elemental specific fills the whole array
  raw = cells(3)
  print '(a, 2f6.1)', 'cells(3)        : ', raw
end program type_bound_demo
raw   = a + b   :    4.5   5.5
raw   = 2 * a   :    6.0   8.0
raw   = a * 0.5 :    1.5   2.0
cells(3)        :    0.5   0.5

Abstract interfaces and dummy procedures

An abstract interface (abstract interface) declares characteristics without a procedure behind it (15.4.1). It is the usual way to give a pointer or dummy procedure a full interface when no actual procedure is at hand, and it is what a deferred type-bound procedure refers to. A procedure declaration statement (15.4.3.6) uses it: procedure(rate_fn) :: f declares f as an external or dummy procedure with that interface, procedure(rate_fn), pointer :: p as a procedure pointer, and procedure(real(dp)) :: g as a function with an implicit interface and the given result type (the interface name can also be an ordinary procedure, a module procedure or an intrinsic from Table 16.2, or a type specification). C1515 requires the name to be an abstract interface or a procedure with an explicit interface.

A dummy procedure is the Fortran way to pass behaviour: the callee calls f(u) and the caller decides what f is. The characteristics of the actual procedure must be the same as those of the dummy, except that a pure actual may be associated with a non-pure dummy (15.5.2.10). The abstract interface below uses import :: dp so that the interface body can see the kind from the module, since an interface body does not see its host.

module heatflow_kinds
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: dp
  integer, parameter :: dp = real64
end module heatflow_kinds

module heatflow_rates
  use heatflow_kinds, only: dp
  implicit none
  private
  public :: rate_fn, advance, newton_cooling, sqrt_cooling

  ! An abstract interface has no procedure behind it: it only names a set
  ! of characteristics that dummy arguments and procedure pointers can share.
  abstract interface
    function rate_fn(u) result(du)
      import :: dp
      real(dp), intent(in) :: u
      real(dp) :: du
    end function rate_fn
  end interface

contains

  ! Explicit Euler with a rate supplied by the caller (a dummy procedure).
  function advance(f, u0, dt, nsteps) result(u)
    procedure(rate_fn) :: f
    real(dp), intent(in) :: u0, dt
    integer, intent(in) :: nsteps
    real(dp) :: u
    integer :: n
    u = u0
    do n = 1, nsteps
      u = u + dt*f(u)
    end do
  end function advance

  ! A module procedure with the characteristics of rate_fn.
  function newton_cooling(u) result(du)
    real(dp), intent(in) :: u
    real(dp) :: du
    real(dp), parameter :: ambient = 20.0_dp, k = 0.5_dp
    du = -k*(u - ambient)
  end function newton_cooling

  ! The specific intrinsic names (DSQRT and friends) are obsolescent in
  ! Fortran 2023 (Annex B.3.12): wrap the generic intrinsic in a procedure
  ! of your own instead of passing the intrinsic itself.
  function sqrt_cooling(u) result(du)
    real(dp), intent(in) :: u
    real(dp) :: du
    du = -sqrt(u)
  end function sqrt_cooling

end module heatflow_rates

program dummy_procedure_demo
  use heatflow_kinds, only: dp
  use heatflow_rates, only: rate_fn, advance, newton_cooling, sqrt_cooling
  implicit none
  procedure(rate_fn), pointer :: chosen

  print '(a, f8.4)', 'module procedure   : ', advance(newton_cooling, 100.0_dp, 0.1_dp, 10)
  chosen => newton_cooling
  print '(a, f8.4)', 'procedure pointer  : ', advance(chosen, 100.0_dp, 0.1_dp, 10)
  print '(a, f8.4)', 'wrapped intrinsic  : ', advance(sqrt_cooling, 100.0_dp, 0.1_dp, 10)
end program dummy_procedure_demo
module procedure   :  67.8990
procedure pointer  :  67.8990
wrapped intrinsic  :  90.2256

Details that surprise people:

  • A specific intrinsic function (dsqrt, amax1, and the others of Table 16.2) may be an actual argument when it is named in an intrinsic statement, but specific intrinsic names are obsolescent in Fortran 2023 (Annex B.3.12). Wrap the generic intrinsic in a procedure of your own, as sqrt_cooling does. A generic intrinsic name used as an actual argument stands for the specific of the same name only.

  • An elemental interface can describe only an external procedure (C1517), so there are no elemental dummy procedures or elemental procedure pointers; a pointer target cannot be a nonintrinsic elemental procedure (C1034, C1519), although an elemental intrinsic may be the target of a nonelemental pointer (10.2.2.4).

  • An optional dummy procedure is tested with present; a dummy procedure with the pointer attribute receives an actual that is a pointer, or a valid target with intent(in).

  • The old way, external f with no interface, declares nothing about the arguments. It still compiles, but an implicit interface cannot be checked: avoid it in new code, use implicit none (type, external) to catch missing declarations, and see Legacy Features and Modernization for old code that uses it.

Procedure pointers

A procedure pointer is a variable that can be associated with a procedure and called through its name. It is declared with a procedure statement that has the pointer attribute, optionally initialized with => null() or with the name of an initial target (which implies save), and associated with a pointer assignment p => target (10.2.2.4, 15.4.3.6). The target must have characteristics that match the pointer’s, except that it may be pure or simple when the pointer is not, and an elemental intrinsic may be the target of a non-elemental pointer. A target cannot be a nonintrinsic elemental procedure (C1034). The intrinsic associated(p) and associated(p, target) work on procedure pointers; null() and nullify disassociate them.

A procedure pointer component of a derived type is declared by procedure(iface), pointer, nopass :: name => null() (7.5.4.1; C761 requires pointer). nopass means the call obj%name(args) passes args only; pass (the default) or pass(arg) means the object is passed as the first dummy argument or the one called arg. C762 requires nopass for a component with an implicit interface or with no arguments. A function may return a procedure pointer.

module heatflow_kinds
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: dp
  integer, parameter :: dp = real64
end module heatflow_kinds

module heatflow_hooks
  use heatflow_kinds, only: dp
  implicit none
  private
  public :: rate_fn, report_sub, probe_t, select_rate, constant_rate, linear_rate

  type :: probe_t
    character(len=12) :: label = 'probe'
    ! A procedure pointer component with NOPASS: no passed-object dummy.
    procedure(rate_fn), pointer, nopass :: rate => null()
    ! With PASS the object itself is passed as the named dummy argument;
    ! the interface (report_sub) has self as its first argument.
    procedure(report_sub), pointer, pass(self) :: report => null()
  end type probe_t

  ! The abstract interfaces come after the type: report_sub refers to probe_t
  ! and probe_t refers to report_sub, and the type is imported by name.
  abstract interface
    function rate_fn(u) result(du)
      import :: dp
      real(dp), intent(in) :: u
      real(dp) :: du
    end function rate_fn

    subroutine report_sub(self, value)
      import :: dp, probe_t
      class(probe_t), intent(in) :: self
      real(dp), intent(in) :: value
    end subroutine report_sub
  end interface

contains

  function constant_rate(u) result(du)
    real(dp), intent(in) :: u
    real(dp) :: du
    du = 0.0_dp*u - 1.0_dp
  end function constant_rate

  function linear_rate(u) result(du)
    real(dp), intent(in) :: u
    real(dp) :: du
    du = -0.5_dp*u
  end function linear_rate

  ! A function can return a procedure pointer.
  function select_rate(name) result(p)
    character(len=*), intent(in) :: name
    procedure(rate_fn), pointer :: p
    select case (name)
    case ('constant')
      p => constant_rate
    case ('linear')
      p => linear_rate
    case default
      p => null()
    end select
  end function select_rate

end module heatflow_hooks

module heatflow_printers
  use heatflow_kinds, only: dp
  use heatflow_hooks, only: probe_t
  implicit none
  private
  public :: print_with_label
contains
  subroutine print_with_label(self, value)
    class(probe_t), intent(in) :: self
    real(dp), intent(in) :: value
    print '(a, a, a, f7.2)', 'probe "', trim(self%label), '" reads ', value
  end subroutine print_with_label
end module heatflow_printers

program procedure_pointer_demo
  use heatflow_kinds, only: dp
  use heatflow_hooks, only: probe_t, rate_fn, select_rate, constant_rate, linear_rate
  use heatflow_printers, only: print_with_label
  implicit none
  type(probe_t) :: probe
  procedure(rate_fn), pointer :: p
  real(dp) :: u

  probe%label = 'centre'
  probe%rate => linear_rate                 ! pointer assignment to a component
  probe%report => print_with_label

  u = 80.0_dp
  call probe%report(probe%rate(u))          ! probe is passed as self; rate gets no object
  print '(a, l2)', 'rate points to linear_rate  :', associated(probe%rate, linear_rate)
  print '(a, l2)', 'rate points to constant_rate:', associated(probe%rate, constant_rate)

  p => select_rate('constant')
  print '(a, f7.2)', 'selected constant rate      :', p(u)
  p => select_rate('unknown')
  print '(a, l2)', 'unknown name gives a pointer:', associated(p)

  probe%rate => null()
  print '(a, l2)', 'after => null(), associated :', associated(probe%rate)
end program procedure_pointer_demo
probe "centre" reads  -40.00
rate points to linear_rate  : T
rate points to constant_rate: F
selected constant rate      :  -1.00
unknown name gives a pointer: F
after => null(), associated : F

Notes:

  • The abstract interface report_sub names the type that contains the procedure pointer that uses it. The two refer to each other, so the type is defined first and the abstract interface follows it with import :: probe_t, while the component names the interface before it is declared. The standard asks for an earlier declaration only when the interface name comes from a procedure declaration statement (C1515), and both compilers accept this order.

  • Invoking a disassociated procedure pointer is not allowed and is not diagnosed by -fcheck=all (the last listing of the next subsection shows what happens). Test with associated before the call when null() is a legitimate state.

  • A procedure pointer component is not a type-bound procedure: it is data, it is not inherited the way a binding is, it can differ from one object to the next, and assigning an object copies the pointer rather than the procedure. Use a deferred binding for polymorphism that all objects of a type share, and a procedure pointer component for per-object callbacks.

  • A table of procedures is an array of a derived type that has one procedure pointer component, because there are no arrays of procedure pointers.

Internal procedures as dummy arguments and targets

Since Fortran 2008 an internal procedure may be an actual argument or the target of a procedure pointer (WG5 N1891, section 10.4), which lets it capture data from its host by host association. The host instance that was current when the association was made is the one the procedure sees (15.6.2.4), so the pointer must not be used after the host returns: that is undefined behaviour and there is no run-time check for it. The program below scans a list of values of a stiffness k; decay reads k from the host scan_stiffness.

module heatflow_kinds
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: dp
  integer, parameter :: dp = real64
end module heatflow_kinds

module heatflow_integrate
  use heatflow_kinds, only: dp
  implicit none
  private
  public :: rate_fn, advance

  abstract interface
    function rate_fn(u) result(du)
      import :: dp
      real(dp), intent(in) :: u
      real(dp) :: du
    end function rate_fn
  end interface

contains

  function advance(f, u0, dt, nsteps) result(u)
    procedure(rate_fn) :: f
    real(dp), intent(in) :: u0, dt
    integer, intent(in) :: nsteps
    real(dp) :: u
    integer :: n
    u = u0
    do n = 1, nsteps
      u = u + dt*f(u)
    end do
  end function advance

end module heatflow_integrate

subroutine scan_stiffness(stiffness)
  use heatflow_kinds, only: dp
  use heatflow_integrate, only: rate_fn, advance
  implicit none
  real(dp), intent(in) :: stiffness(:)
  procedure(rate_fn), pointer :: p
  real(dp) :: k
  integer :: i

  p => decay                       ! an internal procedure as a pointer target
  do i = 1, size(stiffness)
    k = stiffness(i)               ! decay sees the current value of k
    print '(a, f4.1, a, f8.4)', 'k = ', k, ' -> ', advance(p, 100.0_dp, 0.05_dp, 10)
  end do
  ! p must not be used after this subroutine returns: its host instance
  ! (and so k) ceases to exist (15.6.2.4).

contains

  function decay(u) result(du)
    real(dp), intent(in) :: u
    real(dp) :: du
    du = -k*u
  end function decay

end subroutine scan_stiffness

program internal_target_demo
  use heatflow_kinds, only: dp
  implicit none
  interface
    subroutine scan_stiffness(stiffness)
      import :: dp
      real(dp), intent(in) :: stiffness(:)
    end subroutine scan_stiffness
  end interface

  call scan_stiffness([0.5_dp, 1.0_dp, 2.0_dp])
end program internal_target_demo

It was built with -ftrampoline-impl=heap added to the usual flags; the output is the same as without:

k =  0.5 ->  77.6330
k =  1.0 ->  59.8737
k =  2.0 ->  34.8678

GNU Fortran implements an internal procedure used this way with a trampoline, a small piece of code on the stack. On Linux the linker then warns about an executable stack unless the trampoline is allocated on the heap. Without the extra option the same program links with this warning (Flang prints a similar one):

/usr/bin/ld: warning: /tmp/cczOFf0O.o: requires executable stack (because the .note.GNU-stack section is executable)
link-done

-ftrampoline-impl=heap (a GCC option, available on x86_64 and aarch64 Linux and x86_64 Darwin, see the GCC code-generation options) removes the executable-stack requirement at the price of a heap allocation. Passing a module procedure instead of an internal one avoids the trampoline completely, and is the better design whenever the data can be passed as an argument.

The last listing is deliberately erroneous: it calls a disassociated procedure pointer. The standard puts no requirement on the result; GNU Fortran 16.2 (with -fcheck=all) and Flang 23 both end in a segmentation fault, and -fcheck=all does not catch it.

! NON-CONFORMING ON PURPOSE: the procedure pointer is disassociated when it
! is referenced. The standard puts no requirement on what happens next.
program null_procedure_pointer
  implicit none
  abstract interface
    function rate_fn(u) result(du)
      real, intent(in) :: u
      real :: du
    end function rate_fn
  end interface
  procedure(rate_fn), pointer :: p => null()
  real :: u
  u = 80.0
  print '(a, l2)', 'associated: ', associated(p)
  print '(a, f8.2)', 'p(u)      : ', p(u)
end program null_procedure_pointer
associated:  F

Program received signal SIGSEGV: Segmentation fault - invalid memory reference.

Backtrace for this error:
#0  0xffff9096196f in ???
#1  0xffff909605df in ???
#2  0xffff90b3b7ff in ???
#3  0x0 in ???
Segmentation fault

Compiler support and diagnostics

Feature GNU Fortran 16.2 LLVM Flang 23 Notes

Generic interface block, module procedure, procedure

yes

yes

all examples above

generic statement outside a type (F2023)

yes

yes

example 01_generic_rms

Defined operators, assignment, generic constructor named like the type

yes

yes

examples 04, 05, 10

value for an operand of a defined operation (F2023)

no, error

yes

example 11

Procedure pointers, pointer components, pass and nopass

yes

yes

example 07

Internal procedure as actual argument or target

yes, with trampoline

yes, with trampoline

executable-stack linker warning; -ftrampoline-impl=heap for gfortran

Ambiguous generic

compile error, "Ambiguous interfaces in generic interface"

compile error

errors above are from GNU Fortran

Keyword call to an implicit interface

compile error

compile error

err_04

Call of a disassociated procedure pointer

SIGSEGV

SIGSEGV

no run-time check

Other vendors (Intel ifx, NAG, Cray) were not available for this page and are not described here; check the compiler’s own table of Fortran 2023 features before depending on the two Fortran 2023 items above. The compiler flags that make these mistakes visible are described in Compilers and Compiler Options. For a source-level style guide see Best Practices and Style.

Common mistakes

Mistake What happens and what to do

Specifics that differ only by dummy argument name, intent or result type

ambiguous generic; add a type, kind or rank difference to a nonoptional argument

Making the distinguishing argument optional

ambiguous generic; keep it nonoptional, or give the specifics different required arguments

Positional distinction without keyword distinction (swapped argument order of the same types)

rejected by the fourth rule of C1514; rename or retype one argument

Operator function with an argument that is not intent(in) or value

error; the operands are expressions and cannot be changed

Defined assignment between two default reals or two default integers

error; intrinsic assignment cannot be redefined

a .dot. b + c expecting a .dot. (b + c)

the defined binary operator is the loosest; parenthesize

Selecting a component of an operator result, (a + b)%qx

syntax error; assign to a variable or call a helper first

Comparing real components with == in operator(==)

false negatives from rounding; compare with a documented tolerance

Calling an external procedure with keywords and an implicit interface

compile error; give it an interface body or put it in a module

Passing an internal procedure out of its host, or keeping a pointer to it after the host returns

undefined behaviour; no run-time check

Calling a procedure pointer that is not associated

segmentation fault; test associated(p)

Assuming a procedure pointer or dummy procedure with an implicit interface is checked

it is not; use an abstract interface

Listing a specific procedure twice under one generic name

error (C1509, C1510)

Using the specific intrinsic name (dsqrt) as an actual argument

obsolescent in Fortran 2023; wrap the generic intrinsic in your own procedure

Where the books are outdated

The reference book for this section, Modern Fortran Explained: Incorporating Fortran 2023, covers the 2023 standard, including the generic statement outside a type definition, and the sections consulted for this page (generic interfaces, defined operators, defined assignment, abstract interfaces and procedure pointers) agree with the standard. The points to know are the following.

  • The book’s appendix of the authors' own recommendations advises against .eq.-style operators and against implicit interfaces. These are the authors' preferences and not deprecations in the standard: .eq. and == are the same operator (10.1.6.2), and an implicit interface is still valid, though implicit none (external) can be used to rule it out.

  • The examples give operand dummy arguments intent(in), which was the only choice for a long time; Fortran 2023 also permits value (15.4.3.4.2), but GNU Fortran 16.2 does not yet accept it, as shown above.

  • The book is about the language, not about compilers. The internal-procedure trampoline and its executable-stack warning, the -ftrampoline-impl=heap option, the missing run-time check for a disassociated procedure pointer, and the compilers' support for the Fortran 2023 items above are practical matters of GNU Fortran and Flang that this page supplies.

  • Older tutorials, from the Fortran 90 and 95 era, show only interface blocks with module procedure, do not allow an internal procedure as an actual argument or pointer target (allowed since Fortran 2008), and state that two specifics cannot be told apart by pointer versus allocatable or by a procedure versus a data object (both added in Fortran 2008) or by the number of dummy procedures (added in Fortran 2018). They also pass specific intrinsic names such as dsqrt as actual arguments, which Fortran 2023 declares obsolescent.

  • The MATLAB book’s Fortran examples declare the gateway routine’s arguments with old-style type statements instead of interfaces; the MATLAB page uses explicit interfaces throughout.

References