Variables, Declarations and Attributes

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, working from the final draft J3/23-007r1). Its examples were compiled and run on 2026-10-18 with GNU Fortran 16.2 (-std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all) and LLVM Flang 23, and the output shown is the real output. Declarations are where a Fortran program says what every name is: its type, kind, shape, lifetime, visibility and how it may be used. Because the language was designed for numerical code that lives for decades, the rules are strict in some places (a dummy argument declared intent(in) really cannot be assigned) and surprisingly permissive in others (a misspelt name used to become a new variable without any diagnostic). This page covers the type declaration statement and its attributes, implicit none, named constants, initialization, specification expressions, the block construct and the Fortran 2023 typeof and classof declarations.

The intrinsic types and kind parameters used in declarations are in Intrinsic Types and Kinds; the executable statements that use the variables are in Expressions and Assignment. Array shapes, allocation, pointers and derived types have their own pages (Arrays Fundamentals, Allocatable Arrays and Dynamic Memory, Pointers and Targets and Derived Types); here they appear only as attributes. The scoping units that contain declarations are described in Program Structure and Source Form and Modules and Submodules. Readers who know C will recognise parts of this page in Storage Duration, Scope and Linkage and Constants, Enumerations and Initialization; the main difference is that Fortran declarations carry intent and aliasing information that a C compiler has to infer or be told with const and restrict.

The type declaration statement

A type declaration statement (S 8.2, rule R801) has the form:

declaration-type-spec [ [ , attr-spec ] ... :: ] entity-decl-list
  • The declaration-type-spec is an intrinsic type with its parameters (integer(ip), real(dp), character(len=*)), a derived type (type(grid_t)), a polymorphic type (class(solver_t), class(*)), type(*) for assumed-type dummies, or, since Fortran 2023, typeof(x) or classof(x) (see The rank clause, typeof and classof (Fortran 2023)).

  • The attr-spec list gives attributes that apply to every entity in the statement. The attributes are listed in Attribute reference. The same attribute shall not appear twice in one statement (C801), and the :: separator is required whenever there is an attribute or an initialization (C806).

  • Each entity-decl (R803) is a name followed by an optional array-spec in parentheses, an optional coarray-spec in brackets, an optional *len character length and an optional initialization. An array-spec or coarray-spec on one entity overrides the dimension or codimension attribute for that entity only; a *len overrides the character length for that entity only.

  • With parameter, every entity must be initialized (C807). Dummy arguments, function results, automatic objects and allocatable variables cannot be initialized (C808).

The first example declares named constants and arrays with the heatflow kinds module (dp is real64, ip is int32), and shows the per-entity overrides:

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

program declarations
  use heatflow_kinds, only: dp, ip
  implicit none

  ! Attributes in the attribute list apply to every entity in the statement.
  real(dp), parameter :: lx = 1.0_dp, ly = 0.5_dp
  integer(ip), parameter :: nx = 5_ip, ny = 3_ip

  ! dimension in the attribute list gives a default shape ...
  real(dp), dimension(nx, ny) :: u, unew
  ! ... that an array-spec in the entity-decl overrides (here: a vector).
  real(dp), dimension(nx, ny) :: u0, probe(nx)

  ! A character length can be overridden per entity with *len.
  character(len=8) :: label, short*4

  ! Initialization appears in the entity-decl (and needs the :: separator).
  integer(ip) :: step = 0_ip
  logical :: converged = .false.

  u = 0.0_dp
  unew = 0.0_dp
  u0 = 20.0_dp
  probe = u0(:, 1)
  label = 'plate'
  short = 'abcd'

  print '(a, 2i3)', 'shape of u:        ', shape(u)
  print '(a, 2i3)', 'shape of u0:       ', shape(u0)
  print '(a, i3)', 'size of probe:     ', size(probe)
  print '(a, i3, a, i3)', 'len(label):        ', len(label), '  len(short): ', len(short)
  print '(a, a)', 'short:              ', short
  print '(a, i3, l2)', 'step, converged:   ', step, converged
  print '(a, f5.2)', 'dx:                ', lx/real(nx - 1_ip, dp)
  print '(a, f5.2)', 'dy:                ', ly/real(ny - 1_ip, dp)
end program declarations
shape of u:          5  3
shape of u0:         5  3
size of probe:       5
len(label):          8  len(short):   4
short:              abcd
step, converged:     0 F
dx:                 0.25
dy:                 0.25

The dimension override is easy to miss when reading code: u0 has the shape (5, 3) from the attribute list, while probe(nx) has the shape (5) because its own array-spec wins. The same applies to short*4, which is a character(len=4) although the statement begins with character(len=8). The statement is accepted without warnings by both compilers. Assigning a longer string to a shorter character variable truncates it silently in the standard, and gfortran reports it with -Wcharacter-truncation.

Some practical rules for declarations follow from the standard.

  • All declarations belong in the specification part, before the first executable statement, with the order use, import, implicit, then other declarations. In a block construct the same applies inside the block (see The block construct).

  • A name in a type declaration statement must not already have a different type: giving a variable a type twice is an error, and a variable typed by the implicit rules can only be confirmed, never changed, by a later declaration (S 8.7).

  • The old form without :: (real x, y(10)) is still valid when there are no attributes and no initialization, and it is what most legacy code uses. New code should always write the :: separator. It is required as soon as an attribute or an initializer is added, and mixing styles invites mistakes.

  • character*20 s and double precision are older spellings. character*n is obsolescent in the standard (S B.3.8); double precision is not obsolescent but is replaced by real(dp) with a kind from iso_fortran_env, as described in Intrinsic Types and Kinds.

Implicit typing and implicit none

Historically a name that was not declared was given a type from its first letter: i to n were default integer and every other letter default real. That mapping is still the default in every program unit and interface body in Fortran 2023 (S 8.7), so this program is standard-conforming, compiles, and does the wrong thing silently. It is shown here as a deliberately legacy example, which does not follow the conventions of this section (no implicit none):

! Deliberately legacy code: there is no IMPLICIT NONE, so a misspelt name is
! silently a new implicitly typed variable. Do not write programs like this.
program implicit_legacy
  total = 0.0
  do i = 1, 3
    total = total + 0.5
  end do
  ! Typing error: "totl" is a brand new variable of default real, value undefined.
  half = totl / 2.0
  n = 7
  x = 7 / 2
  print *, 'i is ', i, ' (letters I-N are integer)'
  print *, 'total =', total
  print *, 'x = 7/2 gives', x, ' n/2 gives', n/2
  print *, 'a typo-variable was compiled without a single error'
end program implicit_legacy

Compiled with gfortran -std=f2023 -Wall -Wextra, it produced two warnings (-Winteger-division for 7 / 2 and -Wuninitialized for totl) and this output:

 i is            4  (letters I-N are integer)
 total =   1.50000000
 x = 7/2 gives   3.00000000      n/2 gives           3
 a typo-variable was compiled without a single error

totl is a different, never-assigned variable of default real, i is an integer, x is a real that received the result of an integer division, and none of that is an error. Flang compiles and runs the same program without any warning at all.

implicit none

implicit none removes the implicit mapping for the scoping unit. It must appear after any use statements and before any other declaration (C895), at most once per scoping unit. Module procedures, internal procedures and block constructs use the mapping of their host, so one implicit none at the top of a module or program covers everything it contains. Interface bodies and external subprograms do not inherit, so each needs its own.

The same code with implicit none is rejected with a precise message:

! Intentionally wrong: shows what implicit none reports for the typo of 02.
program error_typo
  implicit none
  real :: total, half
  total = 1.5
  half = totl/2.0
  print *, half
end program error_typo

This example is intentionally invalid, to show the diagnostic. GNU Fortran 16.2 reports:

05_error_typo.f90:6:13:

    6 |   half = totl/2.0
      |             1
Error: Symbol 'totl' at (1) has no IMPLICIT type; did you mean 'total'?

LLVM Flang 23 reports error: No explicit type declared for 'totl'. Both compilers also offer a switch that behaves as if every procedure began with implicit none: -fimplicit-none, accepted by both GNU Fortran and Flang. The convention of this section is to put implicit none in the source and keep the flag in the compiler options as a safety net for files that forgot it.

implicit none (type, external)

Fortran 2018 added an optional list to the statement: implicit none (type, external). The type item is the classic behaviour. The external item requires every external or dummy procedure used in the scoping unit to have an explicit interface or the external attribute (C899), so that a call to an undeclared external procedure becomes an error instead of an implicit interface with unchecked arguments. Writing implicit none alone is the same as implicit none (type), and in both forms no other implicit statement may appear in the scoping unit (C897). A list of only external leaves the implicit typing rules in force. This page and the rest of the section use the two-item form in new code that calls external procedures, and the plain form elsewhere.

The next program has one external subprogram, an interface body that gives the caller an explicit interface, and implicit none (type, external) in both program units:

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

! An external procedure (no module): its interface is implicit at the call site.
subroutine scale_field(u, factor)
  use heatflow_kinds, only: dp
  implicit none (type, external)
  real(dp), intent(inout) :: u(:)
  real(dp), intent(in) :: factor
  u = u*factor
end subroutine scale_field

program implicit_none_external
  use heatflow_kinds, only: dp
  implicit none (type, external)
  ! With (external) every external procedure used here needs an explicit
  ! interface or the external attribute; an interface body provides it.
  interface
    subroutine scale_field(u, factor)
      import :: dp
      real(dp), intent(inout) :: u(:)
      real(dp), intent(in) :: factor
    end subroutine scale_field
  end interface
  real(dp) :: u(3)

  u = [1.0_dp, 2.0_dp, 3.0_dp]
  call scale_field(u, 2.0_dp)
  print '(a, 3f6.1)', 'scaled: ', u
end program implicit_none_external
scaled:    2.0   4.0   6.0

The interface body needs an import statement naming dp to make the kind from the host module visible; an interface body does not see its host’s names otherwise. The best answer for new code is a module procedure, which always has an explicit interface (see Modules and Submodules); the example uses an external procedure only because that is where implicit none (external) matters.

Without the interface body, the call is rejected. This second example is intentionally invalid:

! Intentionally non-conforming under implicit none (type, external): the call
! to the external subroutine has neither an interface body nor the external attribute.
program error_implicit_external
  implicit none (type, external)
  real :: u(3) = [1.0, 2.0, 3.0]
  call scale_field(u, 2.0)
  print *, u
end program error_implicit_external
04_error_implicit_external.f90:6:26:

    6 |   call scale_field(u, 2.0)
      |                          1
Error: Procedure 'scale_field' called at (1) is not explicitly declared

Flang says 'scale_field' is an external procedure without the EXTERNAL attribute in a scope with IMPLICIT NONE(EXTERNAL). Flang’s switch for the same check is -fimplicit-none-ext; GNU Fortran has no separate switch for it, so the statement in the source is the portable way to get it.

Other uses of the implicit statement

implicit integer (a-h) and similar statements remap the letters. They are valid but can never be combined with implicit none, and the authors of the Metcalf-Reid-Cohen-Bader reference (MRCB App. A.9) recommend against them; that is the authors' advice, not an obsolescence in the standard. Under implicit none every name needs an explicit type before it is used in a specification expression or a parameter statement, so keep the explicit type declaration first.

Status in the standard

Default implicit typing is not obsolescent in Fortran 2023. The Fortran 2028 working draft J3/26-007r1 adds it to the list of obsolescent features (Annex B.3.15: "a significant source of programming errors"). It is still described only as upcoming here, because Fortran 2028 is at committee-draft stage and is not published. The practical advice is the same either way: every program unit and interface body should begin with implicit none. See Legacy Features and Modernization and What’s New and Fortran 2028 Preview.

Attribute reference

An attribute is a property of an entity other than its type, and Fortran has more of them than most languages because it separates what a name is (type, kind, shape) from how it may be used (intent, aliasing, visibility). The attributes of S 8.5 are given in the attribute list of a type declaration statement or in a separate attribute specification statement (S 8.6, see Attribute specification statements). The table lists each with its clause, where it can appear, what it does and the standard in which it appeared.

Attribute S clause Applies to Effect and main constraints Since

parameter

8.5.13

Named constants, any intrinsic or derived type

The entity is a constant defined by a constant expression and cannot be a variable, coarray or procedure (C852). Must be initialized (C807).

F77 (statement); F90 (attribute)

dimension(spec)

8.5.8

Variables, named constants, dummies, function results

Gives the rank and bounds: explicit shape, assumed shape (:), deferred shape (:) for allocatables and pointers, assumed size (*), implied shape (*) for named constants and assumed rank (..).

F90

allocatable

8.5.3

Variables and components (also dummies and function results)

Storage is created by allocate or by assignment and released automatically. Cannot be combined with pointer.

F90 (local arrays); TR 15581 and F2003 (dummies, results, components, scalars)

pointer

8.5.14

Variables, components, dummies, function results, procedures

Can be associated with a target during execution. Cannot be combined with allocatable, target or intrinsic, and cannot be a coarray (C854).

F90

target

8.5.18

Variables

Allows pointers to refer to the object. Cannot be combined with pointer (C866).

F90

intent(in), intent(out), intent(inout)

8.5.10

Dummy data objects and dummy procedure pointers

Declares the intended direction of data flow; the compiler can reject assignments and passing an unsuitable actual argument.

F90

optional

8.5.12

Dummy arguments

The argument may be omitted in the call; test with present.

F90

value

8.5.19

Dummy data objects

Pass by value: the dummy is a private copy. Not allowed with allocatable, pointer, volatile, intent(out) or intent(inout) (C868). Used mainly with bind(c).

F2003

save

8.5.16

Local variables, common blocks, procedure pointers

The variable keeps its value, allocation and association status between invocations. Not allowed for dummies, function results or automatic objects (C862, C814).

F77

protected

8.5.15

Module variables and procedure pointers

Readable through use association but definable only inside the module (and its submodules) (C859).

F2003

public, private

8.5.2

Entities in a module specification part

Controls what other scoping units can access by use association; also applies to components and type names (C817).

F90

contiguous

8.5.7

Array pointers, assumed-shape arrays and assumed-rank dummies

Guarantees unit-stride storage, which allows pointer rank remapping and avoids copy-in/copy-out (C830).

F2008

asynchronous

8.5.4

Variables

The variable may take part in asynchronous input/output or asynchronous communication; disables code motion around it.

F2003

volatile

8.5.20

Variables that are not intent(in) dummies

May be referenced or changed by means outside the program (hardware, another thread); every access goes to memory.

F2003

codimension[spec]

8.5.6

Variables and components

Declares a coarray. The sum of rank and corank cannot exceed 15 (C822). Local coarrays must be save or allocatable.

F2008

bind(c [, name=...])

8.5.5

Module variables, common blocks, types, procedures

Interoperates with a C variable, type or function (C818-C821).

F2003

rank(n)

8.5.17

Dummy data objects, allocatables and pointers

Declares the rank with a constant expression instead of an array-spec. Equivalent to dimension with the right number of colons.

F2023

external, intrinsic

8.5.9, 8.5.11

Procedures

Marks a name as an external procedure or as an intrinsic one (mutually exclusive, C842). Covered on the procedures page.

F77

The restrictions in the table give the usual rejected combinations: pointer with allocatable or target, value with intent(out), protected outside a module, save on a dummy, parameter on a variable. These are constraints of the standard, and every conforming compiler must diagnose them.

intent

intent says how a dummy argument is used (S 8.5.10). intent(in) means the dummy shall not be defined or become undefined during the invocation, intent(out) means it becomes undefined on entry (except default-initialized components) and the procedure is expected to define it, and intent(inout) means both. A nonpointer intent(in) object cannot appear in a variable definition context (C846), which includes assignment, being the target of read, and being passed to another dummy that is intent(out) or intent(inout). For a pointer dummy, intent(in) constrains the association status, not the target: you may assign to the pointee, you may not re-point it (C847).

  • An allocatable dummy with intent(out) has its actual argument deallocated when the procedure is invoked (S 9.7.3.2). Passing a large buffer to such a dummy therefore frees it, which is a frequent source of "my array disappeared" reports.

  • The actual argument for an intent(out) or intent(inout) dummy must be definable: a literal or an expression is rejected at the call site.

  • A dummy without intent is not an error in the standard, but it gives up the compiler checks and the optimiser’s aliasing information. This page’s examples give every dummy an intent; the best-practice reasoning is in Best Practices and Style.

An intentionally invalid example shows the diagnostic when an intent(in) dummy is assigned:

! Intentionally invalid: assigns to an intent(in) dummy argument.
module counter
  implicit none
contains
  subroutine bump(by)
    integer, intent(in) :: by
    by = 0
  end subroutine bump
end module counter
12_error_intent.f90:7:5:

    7 |     by = 0
      |     1~~
Error: Dummy argument 'by' with INTENT(IN) in variable definition context (assignment) at (1)

Flang reports Left-hand side of assignment is not definable and, as the reason, 'by' is an INTENT(IN) dummy argument. Both compilers stop at compile time; there is no runtime check for a violation of intent.

value, optional, protected, contiguous and rank

The following example, a module for tracking a run, shows several attributes working together. The module is private by default and lists its public names; steps_taken is protected, so other units can read it but only the module can change it; advance takes a value argument whose changes stay local; scaled_copy has an optional argument, tested with present; and norm_inf receives a contiguous, target array and builds a rank-one pointer onto it with bounds remapping, which the standard allows only for contiguous targets.

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

module run_state
  use heatflow_kinds, only: dp, ip
  implicit none
  private                                  ! everything private unless listed below

  public :: steps_taken, reset_counter, advance, scaled_copy, norm_inf, first_row_sum
  integer(ip), protected :: steps_taken = 0_ip   ! readable outside, writable only here
  real(dp), parameter, public :: half = 0.5_dp   ! accessibility inside the attribute list

contains

  subroutine reset_counter()
    steps_taken = 0_ip
  end subroutine reset_counter

  ! value: the dummy is a private copy; the actual argument can be an expression.
  subroutine advance(by)
    integer(ip), value :: by
    by = by*2_ip                            ! legal: changes only the local copy
    steps_taken = steps_taken + by
  end subroutine advance

  ! intent, optional and present(); intent(out) makes y undefined on entry.
  subroutine scaled_copy(x, y, factor)
    real(dp), intent(in) :: x(:)
    real(dp), intent(out) :: y(:)
    real(dp), intent(in), optional :: factor
    y = x
    if (present(factor)) y = y*factor
  end subroutine scaled_copy

  ! contiguous guarantees unit stride; target allows pointers to refer to it.
  function norm_inf(u) result(r)
    real(dp), intent(in), contiguous, target :: u(:, :)
    real(dp) :: r
    real(dp), pointer, contiguous :: flat(:)
    flat(1:size(u)) => u                     ! pointer rank remapping needs a contiguous target
    r = maxval(abs(flat))
  end function norm_inf

  ! An ordinary assumed-shape dummy; the rank clause equivalent is shown in the next example.
  function first_row_sum(u) result(s)
    real(dp), intent(in) :: u(:, :)
    real(dp) :: s
    s = sum(u(1, :))
  end function first_row_sum
end module run_state

program attributes
  use heatflow_kinds, only: dp, ip
  use run_state
  implicit none
  integer(ip) :: k
  real(dp) :: x(3), y(3), u(2, 3)

  k = 5_ip
  call advance(k)
  call advance(3_ip)                        ! a constant is fine for a value dummy
  print '(a, i0, a, i0)', 'k after advance: ', k, '   steps_taken: ', steps_taken

  x = [1.0_dp, -4.0_dp, 2.0_dp]
  call scaled_copy(x, y)
  print '(a, 3f6.1)', 'copy:          ', y
  call scaled_copy(x, y, factor=half)
  print '(a, 3f6.1)', 'scaled by half:', y

  u = reshape([1.0_dp, 2.0_dp, 3.0_dp, 4.0_dp, 5.0_dp, -6.0_dp], [2, 3])
  print '(a, f6.1)', 'norm_inf:      ', norm_inf(u)
  print '(a, f6.1)', 'first row sum: ', first_row_sum(u)
  call reset_counter()
  print '(a, i0)', 'steps_taken after reset: ', steps_taken
end program attributes
k after advance: 5   steps_taken: 16
copy:             1.0  -4.0   2.0
scaled by half:   0.5  -2.0   1.0
norm_inf:         6.0
first row sum:    9.0
steps_taken after reset: 0

Points worth noting in this example:

  • advance(k) doubles its private copy and leaves k untouched, so k is still 5 while steps_taken increased by 10 and then by 6. A value dummy can be given a literal or an expression (advance(3_ip)), which an intent(inout) dummy cannot.

  • y is intent(out) in scaled_copy. Because it is an ordinary array, the dummy simply becomes undefined on entry; for an allocatable dummy the actual argument would be deallocated.

  • An absent optional argument must not be referenced, so factor is only used behind present. Passing an absent optional on to another procedure’s optional dummy is allowed and keeps it absent.

  • The module-wide private statement, then public :: a, b for the entities that make up the interface, is the usual way to keep an interface small. Attribute protected is specified only in a module and checked by the compiler on every use (C856, C859).

Protection is a compile-time check. This intentionally invalid program assigns to the protected variable from the main program:

! Intentionally invalid: assigns to a protected module variable outside its module.
module counter
  implicit none
  integer, protected :: steps = 0
contains
  subroutine bump(by)
    integer, intent(in) :: by
    steps = steps + by
  end subroutine bump
end module counter

program error_protected
  use counter
  implicit none
  call bump(1)
  steps = 10
  print *, steps
end program error_protected
14_error_protected.f90:16:3:

   16 |   steps = 10
      |   1~~~~~
Error: Variable 'steps' is PROTECTED and cannot appear in a variable definition context (assignment) at (1)

Flang says 'steps' is protected in this scope and names the use statement that made it visible.

volatile and asynchronous

volatile (S 8.5.20) tells the compiler that something outside the Fortran program can read or write the variable: memory-mapped hardware, a signal handler, or a thread started from C. Every access must therefore go to memory, which switches off most optimisation of that variable. asynchronous (S 8.5.4) is narrower: the variable may be the target of asynchronous input/output or of an asynchronous communication that is still pending, so the compiler must not move accesses across the statements that start and complete the transfer. Its use in asynchronous I/O is on Derived-Type and Asynchronous I/O. The authors of MRCB (App. A.15) recommend asynchronous over volatile where it is enough; again that is a recommendation, volatile is not obsolescent. In a block construct, an asynchronous or volatile statement that names a variable of the host gives that variable the attribute inside the construct only (S 11.1.4).

codimension and bind

codimension declares a coarray and is discussed in Coarrays, Images and Synchronization. A coarray local to a procedure must be allocatable or save, because an automatic coarray would need an implicit synchronisation on entry (S 8.5.6, C826). Fortran 2023 also allows a data object with a coarray component to be an array or allocatable. bind(c) on a variable, type or procedure is described in Interoperability with C.

The rank clause, typeof and classof (Fortran 2023)

Fortran 2023 adds three declaration forms that avoid repeating information (S 7.3.2.1 and S 8.5.17):

  • rank(n) declares a dummy, allocatable or pointer by rank with a constant expression. In the example below, the dummy a is declared real(dp), rank(2), intent(in), an assumed-shape array of rank 2, and the local t is declared real(dp), allocatable, rank(rank(f%u)), so it has the rank of another object. The rank can be 0 for a scalar.

  • typeof(x) declares an entity with the same declared type and type parameters as x (a non-polymorphic type). x must be previously declared, may not be unlimited polymorphic or of abstract type (C711), and typeof may be used only in a declaration (C709).

  • classof(x) is the polymorphic counterpart: an entity of the same declared type as x, polymorphic. It cannot be used for intrinsic-type x (C712).

These forms are most useful in generic code, where a variable must match the type of a dummy without naming it. They are also the Fortran 2023 building blocks for the generic programming facilities described in Generic and Rank Independent Programming.

Compiler support: GNU Fortran 16.2 rejects both rank(n) and typeof/classof (the diagnostics are Invalid character in name and Mangled derived type definition), so the program below was compiled and run with LLVM Flang 23 only and is not checked with -std=f2023 on gfortran:

! Fortran 2023 rank clause and typeof/classof.
! gfortran 16.2 rejects both; LLVM Flang 23 accepts them.
module heatflow_kinds
  use, intrinsic :: iso_fortran_env, only: int32, int64, real64
  implicit none
  private
  public :: dp, ip
  integer, parameter :: dp = real64
  integer, parameter :: ip = int32
end module heatflow_kinds

module probes
  use heatflow_kinds, only: dp, ip
  implicit none
  private
  public :: field_t, total, describe

  type :: field_t
    integer(ip) :: nx = 0_ip
    real(dp), allocatable :: u(:, :)
  end type field_t

contains

  ! rank(2) declares an assumed-shape dummy of rank 2, the same as a(:, :).
  function total(a) result(s)
    real(dp), rank(2), intent(in) :: a
    real(dp) :: s
    s = sum(a)
  end function total

  subroutine describe(f)
    class(field_t), intent(in) :: f
    typeof(f%nx) :: n                  ! same type and kind as f%nx: integer(ip)
    classof(f) :: copy_of_f            ! same declared type as f; polymorphic
    allocatable :: copy_of_f
    real(dp), allocatable, rank(rank(f%u)) :: t   ! same rank as f%u

    n = f%nx
    allocate (copy_of_f, source=f)
    allocate (t(n, n))
    t = 1.0_dp
    print '(a, i0, a, i0, a, f4.1)', 'n = ', n, ', rank of t = ', rank(t), ', sum(t)/n**2 = ', sum(t)/real(n*n, dp)
    print '(a, l2)', 'copy has same type as f: ', same_type_as(copy_of_f, f)
  end subroutine describe

end module probes

program rank_typeof_classof
  use heatflow_kinds, only: dp, ip
  use probes
  implicit none
  type(field_t) :: f
  real(dp) :: a(2, 3)

  f%nx = 3_ip
  allocate (f%u(3, 3))
  f%u = 0.0_dp
  a = reshape([1.0_dp, 2.0_dp, 3.0_dp, 4.0_dp, 5.0_dp, 6.0_dp], [2, 3])
  print '(a, f5.1)', 'total(a) = ', total(a)
  call describe(f)
end program rank_typeof_classof
total(a) =  21.0
n = 3, rank of t = 2, sum(t)/n**2 =  1.0
copy has same type as f:  T

Check each compiler’s own Fortran 2023 support table (Intel ifx, NAG, Cray) before relying on them. Portable code that has to build everywhere writes the type out.

The block construct

block (S 11.1.4, since Fortran 2008) is an executable construct that can contain its own specification part. Its declarations are construct entities with the scope of the block. It is the Fortran counterpart of declaring a local variable in the middle of a C block.

  • The specification part may contain use, import and any declaration except common, equivalence, intent, namelist, optional, statement functions and value (C1107). The block itself may not begin with a format or data statement (C1109).

  • Specification expressions in the block are evaluated on entry, so a block is the place for automatic arrays sized by a value computed earlier in the procedure. The type parameters are fixed at the block statement and do not change if the variables they use change afterwards (S 8.3).

  • A variable declared in the block with the same name as one outside hides it; the outer variable is unchanged.

  • Unsaved allocatable variables of the block are deallocated when the construct ends. A save variable in a block retains its value and is shared by all instances of the subprogram (S 8.5.16).

  • Implicit typing is not affected by block: the mapping is the one of the host scoping unit. A save statement outside a block does not make the block’s own variables saved.

  • A construct name allows exit to leave the block early, and a branch can leave but not enter it.

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

program block_construct
  use heatflow_kinds, only: dp, ip
  implicit none
  real(dp) :: u(6), residual
  real(dp) :: tmp
  integer(ip) :: n, pass

  u = [0.0_dp, 4.0_dp, 8.0_dp, 8.0_dp, 4.0_dp, 0.0_dp]
  n = size(u, kind=ip)
  tmp = -1.0_dp

  ! A block with its own declarations: they exist only inside the construct.
  smoothing: block
    real(dp) :: work(n)                  ! automatic array sized at run time
    real(dp) :: tmp                      ! hides the outer tmp, never touches it
    integer :: i

    work = u
    do i = 2, int(n) - 1
      tmp = 0.5_dp*(work(i - 1) + work(i + 1))
      u(i) = 0.5_dp*(work(i) + tmp)
    end do
    residual = maxval(abs(u - work))
    if (residual < 1.0e-12_dp) exit smoothing   ! leave the construct early
    print '(a, es9.2)', 'residual after one sweep: ', residual
  end block smoothing

  print '(a, f5.1)', 'outer tmp unchanged: ', tmp
  print '(a, 6f6.2)', 'u: ', u

  ! A saved variable inside a block keeps its value between executions.
  do pass = 1_ip, 3_ip
    block
      integer, save :: visits = 0
      visits = visits + 1
      print '(a, i0, a, i0)', 'pass ', pass, ', visits ', visits
    end block
  end do
end program block_construct
residual after one sweep:  1.00E+00
outer tmp unchanged:  -1.0
u:   0.00  4.00  7.00  7.00  4.00  0.00
pass 1, visits 1
pass 2, visits 2
pass 3, visits 3

The visits variable inside the unnamed block is saved, so it counts the three executions even though a new block is entered each time. Without save it would be recreated on every entry. block is not a function: there is no value, so a block is useful for scoping temporaries and for exit, not for returning results. See Control Flow for exit and construct names.

Named constants and constant expressions

The parameter attribute (S 8.5.13) makes an entity a named constant. Its value comes from a constant expression and its type, kind and shape are fixed at the declaration. A named constant cannot be modified, has no storage that can be accessed by address (it cannot be a target), and may be used wherever a literal of that type is allowed, including kind parameters (real(wide)), array bounds and initializations. A constant expression (S 10.1.12) is built from:

  • literals and other named constants (or their subobjects);

  • array constructors and structure constructors whose elements are themselves constant expressions;

  • elemental standard intrinsic functions of constant arguments (atan, nint, real, selected_real_kind);

  • transformational standard intrinsic functions of constant arguments, apart from command_argument_count, get_team, num_images, team_number and this_image, which are never allowed, and null and transfer, which have rules of their own;

  • specification inquiries on constants (size, kind, digits, tiny, huge, len), and selected inquiries from ieee_arithmetic;

  • the ac-do-variable of an array constructor’s implied do, and a parenthesised constant expression.

No user-defined operation or function may appear. The kind of every operand should be explicit: 4.0_dp*atan(1.0_dp) is a double-precision value of pi, while 4*atan(1.0) is a default-real value that is widened afterwards, losing the extra digits.

An implied-shape array, written with (*) for a named constant, takes its extent from the initializer, so adding an element does not require editing a size. This applies to named constants only (C838).

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

program named_constants
  use heatflow_kinds, only: dp, ip
  implicit none

  ! Constant expressions: literals, other named constants and intrinsic
  ! functions of constant arguments.
  real(dp), parameter :: pi = 4.0_dp*atan(1.0_dp)
  real(dp), parameter :: lx = 1.0_dp, ly = 0.5_dp
  integer(ip), parameter :: nx = 41_ip
  integer(ip), parameter :: ny = nint(real(nx - 1_ip, dp)*ly/lx) + 1_ip

  ! A named array constant; the implied shape (*) takes its extent from the value.
  integer(ip), parameter :: sample_i(*) = [1_ip, (nx - 1_ip)/2_ip + 1_ip, nx]
  ! The kind of a real can itself come from a constant expression.
  integer, parameter :: wide = selected_real_kind(p=15, r=300)
  real(wide), parameter :: tiny_value = tiny(1.0_wide)

  ! Character constants: the length can be assumed from the value.
  character(len=*), parameter :: title = 'heatflow plate'
  character(len=*), parameter :: names(3) = ['dirichlet', 'neumann  ', 'periodic ']

  ! A derived-type constant built from a structure constructor.
  type :: point_t
    real(dp) :: x, y
  end type point_t
  type(point_t), parameter :: origin = point_t(0.0_dp, 0.0_dp)

  ! A constant expression can use array inquiry and transformational functions.
  integer(ip), parameter :: total = sum(sample_i)
  integer, parameter :: nsamples = size(sample_i)

  print '(a, f10.7)', 'pi:            ', pi
  print '(a, i0, a, i0)', 'grid:          ', nx, ' x ', ny
  print '(a, i0, a, 3i4)', 'samples:       ', nsamples, ' -> ', sample_i
  print '(a, i0)', 'sum of samples:', total
  print '(a, i0, a, i0)', 'wide kind:     ', wide, ', digits ', digits(1.0_wide)
  print '(a, es12.3e3)', 'tiny(wide):    ', tiny_value
  print '(3a, i0)', 'title:         "', title, '", length ', len(title)
  print '(a, a)', 'names(2):      ', trim(names(2))
  print '(a, 2f4.1)', 'origin:        ', origin%x, origin%y
end program named_constants
pi:             3.1415927
grid:          41 x 21
samples:       3 ->    1  21  41
sum of samples:63
wide kind:     8, digits 53
tiny(wide):      2.225E-308
title:         "heatflow plate", length 14
names(2):      neumann
origin:         0.0 0.0

Notes on the example:

  • The named constant title, declared with character(len=*), takes its length from the value, and a character array constant names takes the length from the longest element, which is why the shorter strings are padded with blanks to the same length by hand; the standard requires all elements of an array constructor to have the same length unless the constructor begins with a type specification (the type and length followed by a double colon).

  • wide is a kind obtained from selected_real_kind in a constant expression and then used in real(wide). The kinds module of the project does the same once for the whole program; see Intrinsic Types and Kinds.

  • The middle sample is written (nx - 1_ip)/2_ip + 1_ip so that the division is exact. With nx equal to 41, nx/2_ip + 1_ip truncates the quotient, which gfortran reports with -Winteger-division (part of -Wall).

  • Changing a named constant is an error, as the compilers show:

! Intentionally invalid: a named constant cannot be redefined.
program error_parameter
  implicit none
  integer, parameter :: nx = 41
  nx = 81
  print *, nx
end program error_parameter

This intentionally invalid program is reported by GNU Fortran 16.2 as:

11_error_parameter.f90:5:3:

    5 |   nx = 81
      |   1~~
Error: Named constant 'nx' in variable definition context (assignment) at (1)

Flang says Assignment to constant 'nx' is not allowed. In GNU Fortran, -Wunused-parameter (named constants that are never used) is part of -Wextra, and -Wunused-dummy-argument (unused dummy arguments) is part of -Wall, so a clean build under the flags of this section catches leftover constants and arguments.

The parameter statement (parameter (pi = 3.14159_dp)) is an older spelling. It is valid, but the type must be known before it is used, so with implicit none the type declaration has to come first; MRCB App. A.6 recommends the attribute form, and so do the examples here.

Initialization, implicit save and the data statement

A variable declared with an initializer, as in integer(ip) :: count = 0_ip, is explicitly initialized (S 8.4). Two consequences surprise programmers coming from C or Python:

  • The initialization happens once, before the program starts, not each time the procedure is entered. Explicit initialization of a variable that is not in a common block implies the save attribute (S 8.4), so the variable keeps its value between calls. In C terms, it is static, not an automatic variable assigned on entry.

  • A variable without an initializer is not zero. A local variable that is not saved is undefined on entry to the procedure, and reading it before assigning is a programming error that the standard does not require the compiler to detect.

A variable declared in the specification part of a main program, a module or a submodule implicitly has the save attribute (S 8.5.16). The same holds for a common block in these units. Local variables of other procedures are unsaved, unless they are initialized or named in a save statement. A function result, a dummy argument and an automatic object can never be saved (C862, C814).

The decision for a local variable can be summarised as:

flowchart TD V["Variable declared in a scoping unit"] --> U{"In a main program, module or submodule?"} U -->|yes| A1["Saved implicitly: one copy for the whole run"] U -->|no| I{"Initializer, data statement or save?"} I -->|yes| A2["Saved: one copy shared by all invocations"] I -->|no| D{"Dummy argument, function result or automatic object?"} D -->|yes| A3["Exists only during one invocation"] D -->|no| A4["Unsaved local: undefined on entry, value lost on return"]

The following module contains three counters that differ only in how they start. next_with_init uses an initializer and therefore keeps counting; next_without_init assigns count = 0 in the executable part and so always returns 1; next_with_save declares save without an initializer and sets the first value with a guard, which is the explicit way of saying the same thing as the initializer. ticks is a module variable and is always saved. The program also shows data statements (explicit initialization in the old style, with a repeat factor 2*3.0_dp and an implied do loop for a two-dimensional array) and a pointer initialized to null().

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

module step_counter
  use heatflow_kinds, only: dp, ip
  implicit none
  private
  public :: next_with_init, next_without_init, next_with_save, ticks
  ! Module variables always have the save attribute.
  integer(ip) :: ticks = 0_ip
contains
  function next_with_init() result(n)
    integer(ip) :: n
    integer(ip) :: count = 0_ip      ! explicit initialization implies save
    count = count + 1_ip
    n = count
  end function next_with_init

  function next_without_init() result(n)
    integer(ip) :: n
    integer(ip) :: count
    count = 0_ip                     ! executable assignment: runs on every call
    count = count + 1_ip
    n = count
  end function next_without_init

  function next_with_save() result(n)
    integer(ip) :: n
    integer(ip), save :: count        ! save without initialization: undefined at first
    logical, save :: first = .true.   ! a guard written in the modern style
    if (first) then
      count = 0_ip
      first = .false.
    end if
    count = count + 1_ip
    n = count
    ticks = ticks + 1_ip
  end function next_with_save
end module step_counter

program initialization_and_save
  use heatflow_kinds, only: dp, ip
  use step_counter
  implicit none
  integer :: k
  real(dp) :: a(4)
  integer(ip) :: m(2, 3)
  integer :: i, j
  character(len=3) :: tag(2)
  real(dp), pointer :: p => null()

  ! DATA statement: the same effect as initialization in the declaration.
  data a /1.0_dp, 2.0_dp, 2*3.0_dp/
  data ((m(i, j), i=1, 2), j=1, 3) /1_ip, 2_ip, 3_ip, 4_ip, 5_ip, 6_ip/
  data tag(1) /'abc'/, tag(2) /'xyz'/

  do k = 1, 3
    print '(a, i0, a, 3(i0, 1x))', 'call ', k, ': init/noinit/save = ', &
      next_with_init(), next_without_init(), next_with_save()
  end do
  print '(a, i0)', 'ticks (module variable): ', ticks
  print '(a, 4f4.1)', 'a: ', a
  print '(a, 6i2)', 'm in array element order: ', m
  print '(a, 2(a, 1x))', 'tag: ', tag
  print '(a, l2)', 'p associated: ', associated(p)
end program initialization_and_save
call 1: init/noinit/save = 1 1 1
call 2: init/noinit/save = 2 1 2
call 3: init/noinit/save = 3 1 3
ticks (module variable): 3
a:  1.0 2.0 3.0 3.0
m in array element order:  1 2 3 4 5 6
tag: abc xyz
p associated:  F

Things to remember from the data statement (S 8.6.7):

  • It gives explicit initialization, so the variables are implicitly saved. A variable that also has default initialization (a derived type with component initializers) cannot appear in a data statement.

  • Values are matched in array element order, so data m /1, 2, 3, 4, 5, 6/ fills the first column first; an implied do (((m(i, j), i=1, 2), j=1, 3)) lets the layout be spelled out. A repeat count is written n*value.

  • A data statement among executable statements is obsolescent (S B.3.5). The modern replacement is an initializer in the declaration or, for large tables, a named-constant array and an assignment.

  • A variable may be initialized only once in the whole program: a data statement and an initializer for the same variable conflict.

Compilers differ in what happens to saved and unsaved variables that the program never defines, and none of it is portable. GNU Fortran leaves unsaved locals undefined and puts large local arrays (more than -fmax-stack-var-size, 65536 bytes by default) in static memory, where they are typically zero at program start; LLVM Flang zero-initializes saved variables without initializers as an extension, except scalars of the main program (see Flang: Extensions). Neither behaviour is guaranteed by the standard, so the code that relies on it fails under another compiler or after an optimisation level change.

The options that change storage and initialization are:

Option Compiler Effect

-fno-automatic

GNU Fortran

Treats every local of non-recursive procedures as if save were specified. Hides missing initialization instead of fixing it.

-fmax-stack-var-size=n

GNU Fortran

Largest local array (in bytes) placed on the stack; larger ones use static memory unless the procedure is recursive. Default 65536.

-frecursive

GNU Fortran

No static memory for locals; needed when non-recursive procedures are called from several threads.

-fstack-arrays

GNU Fortran, Flang

Put all arrays of unknown size and array temporaries on the stack; large local arrays then need a larger stack limit.

-finit-local-zero, -finit-real=snan and the other -finit-* options

GNU Fortran

Give unsaved locals a known value, such as zero or a signalling NaN, to expose or tame uninitialized use. They do not initialize pointers or allocatable arrays.

-finit-local=zero or -finit-local-zero

Flang

Initialize locals without an explicit or default initialization.

-fsave-main-program

Flang

Place all main program variables in static memory.

-Wuninitialized

GNU Fortran

Warn about use of an undefined local. Enabled by -Wall, but it needs optimization (-O1 or higher) because the analysis uses the optimizer.

A small intentionally wrong program shows why uninitialized variables survive testing: residual is never assigned when nsteps is 0.

! Intentionally wrong: "residual" is never defined when the loop does not run.
! Compile with -finit-real=snan -ffpe-trap=invalid to make the bug visible.
program error_uninitialized
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  real(dp) :: residual
  integer :: step, nsteps

  nsteps = 0
  do step = 1, nsteps
    residual = 1.0_dp/real(step, dp)
  end do
  if (residual < 1.0e-6_dp) then
    print '(a)', 'converged'
  else
    print '(a)', 'not converged'
  end if
end program error_uninitialized

With the strict flags of this page (no optimization), GNU Fortran 16.2 gives no warning, and the program prints "converged" because the undefined value of residual happened to compare as smaller than the tolerance. With -O2 it warns 'residual' is used uninitialized [-Wuninitialized]. Compiled with -finit-real=snan -ffpe-trap=invalid, the first use of the variable aborts the program:

Program received signal SIGFPE: Floating-point exception - erroneous arithmetic operation.

Backtrace for this error:
...
#3  0x4008b8 in error_uninitialized
	at 16_error_uninitialized.f90:13
Floating point exception

(The frame addresses are omitted; the line and file of the offending comparison are in the backtrace.)

Specification expressions and automatic objects

Array bounds and character lengths of local variables may depend on run-time values if they are written as specification expressions (S 10.1.11). This is how a procedure obtains a work array of the right size without allocate. A restricted expression may use:

  • constants and constant expressions;

  • dummy arguments that are not optional and not intent(out), and objects accessible by host or use association or in common;

  • inquiry functions on them (size, lbound, ubound, len, shape, kind), and present;

  • any standard intrinsic function whose arguments are themselves restricted expressions;

  • references to a specification function: a nonintrinsic function that is pure, not internal, not a statement function and has no dummy procedure (S 10.1.11).

An automatic data object is a nondummy object whose type parameter or array bound depends on a specification expression that is not a constant expression (S 8.3). It is created on entry to the procedure (or on execution of the block statement) and disappears on return. It cannot have the save attribute (C814) or an initializer (C808), and the values in the bound expressions are captured at entry: changing the variables afterwards does not resize the object (S 8.3).

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

module smoothing
  use heatflow_kinds, only: dp, ip
  implicit none
  private
  public :: smooth, padded_width
contains
  ! A specification function: pure, not internal, not an intrinsic. It may
  ! appear in the declarations of other procedures.
  pure function padded_width(n) result(w)
    integer(ip), intent(in) :: n
    integer(ip) :: w
    w = n + 2_ip
  end function padded_width

  subroutine smooth(u, label)
    real(dp), intent(inout) :: u(:, :)
    character(len=*), intent(in) :: label

    ! Automatic objects: their shape and length come from specification
    ! expressions evaluated each time the procedure is entered.
    real(dp) :: work(size(u, 1), size(u, 2))
    real(dp) :: halo(padded_width(int(size(u, 1), ip)))
    character(len=len(label) + 5) :: banner
    integer :: i, j

    work = u
    do j = 2, size(u, 2) - 1
      do i = 2, size(u, 1) - 1
        u(i, j) = 0.25_dp*(work(i - 1, j) + work(i + 1, j) + work(i, j - 1) + work(i, j + 1))
      end do
    end do
    halo = 0.0_dp
    banner = '[' // label // '] ok'
    print '(a, 2i3, a, i3, a, a)', 'work shape', shape(work), '  halo size', size(halo), '  ', banner
  end subroutine smooth
end module smoothing

program specification_expressions
  use heatflow_kinds, only: dp
  use smoothing, only: smooth
  implicit none
  real(dp) :: small(4, 4), large(6, 5)

  small = 0.0_dp
  small(2:3, 2:3) = 8.0_dp
  large = 1.0_dp
  call smooth(small, 'small')
  call smooth(large, 'large plate')
  print '(a, 4f6.2)', 'small(2, :) = ', small(2, :)
end program specification_expressions
work shape  4  4  halo size  6  [small] ok
work shape  6  5  halo size  8  [large plate] ok
small(2, :) =   0.00  4.00  4.00  0.00

The two automatic arrays work and halo and the character variable banner have sizes that come from size(u, 1), a specification function call and len(label). The call to smooth with a 4 by 4 and then a 6 by 5 array shows that the sizes follow the dummy argument at each call.

Practical consequences:

  • Automatic arrays are normally placed on the stack. Large ones can overflow it, which appears as a segmentation fault on entry to the procedure rather than as a Fortran error. Remedies are allocatable local arrays (heap) or raising the stack limit (ulimit -s on Unix systems); -fstack-arrays makes the stack choice explicit in GNU Fortran and in Flang. See Allocatable Arrays and Dynamic Memory and Performance and Memory Layout.

  • If a specification expression is wrong (for instance a negative size), the array is zero-sized, not an error; -fcheck=bounds catches later out-of-range subscripts.

  • An automatic array is not the same as an assumed-shape dummy such as u(:, :) (which has the shape of the actual argument and no storage of its own); the dummy forms are covered in Array Arguments: Assumed Shape, Size and Rank.

  • Evaluation of a specification expression must not call a procedure defined by the subprogram that contains it (S 10.1.11), and the order in which specification expressions are evaluated is processor dependent, so they should not depend on each other’s side effects (they can have none if the functions are pure).

Attribute specification statements

Every attribute can also be given in a separate statement after the declaration (S 8.6): dimension, allocatable, pointer, target, intent, optional, parameter, save, value, protected, public/private, volatile, asynchronous, contiguous and codimension. The module below has the same effect as one declaration per entity but spreads each across several statements; it is valid, but it is harder to read and easier to get wrong.

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

! Attribute specification statements: each attribute is given in a separate
! statement after the type declaration. Every entity could instead be written
! in one declaration, for example "real(dp), parameter, public :: gain = 2.0_dp".
module probe_bank
  use heatflow_kinds, only: dp, ip
  implicit none
  private

  integer(ip) :: nprobes
  real(dp) :: gain
  real(dp) :: readings
  dimension readings(3)
  parameter (nprobes = 3_ip, gain = 2.0_dp)
  public :: nprobes, gain, readings, record
  protected :: readings

contains

  subroutine record(k, reading)
    integer(ip) :: k
    real(dp) :: reading
    value :: k
    intent(in) :: reading
    readings(k) = reading*gain
  end subroutine record

end module probe_bank

program attribute_statements
  use heatflow_kinds, only: ip, dp
  use probe_bank
  implicit none
  integer(ip) :: k

  do k = 1_ip, nprobes
    call record(k, real(k, dp))
  end do
  print '(a, i0, a, f4.1)', 'nprobes = ', nprobes, ', gain = ', gain
  print '(a, 3f5.1)', 'readings: ', readings
end program attribute_statements
nprobes = 3, gain =  2.0
readings:   2.0  4.0  6.0

The rules are: an entity may be given an attribute only once per scoping unit (C815), so dimension readings(3) cannot be repeated; the type must be declared before a parameter statement uses the name; and a dimension statement on an untyped name would create an implicitly typed variable, which implicit none forbids. The authors of MRCB recommend not using the dimension, codimension and parameter statements at all (MRCB App. A.6), because the type declaration then looks like the declaration of a scalar while the shape is declared elsewhere. That is the authors' advice and it is shared by this section: put every attribute in the declaration so the whole description of an entity is in one place. The accessibility statements are the exception: public :: a, b and a bare private are the usual way to define a module interface (as in run_state above).

Compiler support and standards history

Feature Standard Notes

implicit none (type, external)

F2018

Accepted by GNU Fortran 16.2 and Flang 23 (-std=f2023); Flang checks external with -fimplicit-none-ext.

Implicit save of main program, module and submodule variables

F2008

Earlier editions required an explicit save for module variables to stay defined; adding it is harmless.

block construct

F2008

Checked on both compilers (example above).

contiguous, value, protected

F2008, F2003

Checked on both compilers (examples above).

codimension, asynchronous, volatile, bind(c)

F2008, F2003

Not exercised on this page; see the pages that cover them.

rank(n), typeof, classof

F2023

Rejected by GNU Fortran 16.2, accepted by Flang 23; check the Intel and NAG tables before use.

Default implicit typing obsolescent

F2028 (draft J3/26-007r1, B.3.15)

Not obsolescent in F2023; the F2028 text is still a working draft.

dimension, parameter statements, data among executables

F77 and earlier

The data form among executable statements is obsolescent (S B.3.5); the others are valid but discouraged by the MRCB authors.

The standard-level support of other compilers is tracked on the compilers page; see Compilers and Compiler Options for the options used on this page and the per-compiler Fortran 2023 support tables.

Where the books are outdated

The MRCB chapters on declarations (Ch. 8, with Ch. 2 for the basics) take Fortran 2018 as the base language and Chapters 22 and 23 add Fortran 2023. Three points need adding for current practice:

  • MRCB Appendix A lists the authors' deprecations, among them the dimension, codimension and parameter statements, non-default implicit mappings, double precision and volatile. They are recommendations, not entries in the standard’s obsolescent list (Annex B.3), which for declarations contains only character*n, data among executables, assumed character length functions and common/equivalence.

  • The book has no information about compilers. In practice rank(n), typeof and classof (the Fortran 2023 declarations in its Chapters 22 and 23) are currently accepted by LLVM Flang 23 and rejected by GNU Fortran 16.2, as shown above.

  • The authors call implicit typing dangerous (MRCB §8.2.1) and recommend implicit none, but it is not obsolescent in Fortran 2023; the Fortran 2028 working draft J3/26-007r1 makes it obsolescent. Treat implicit none as mandatory.