Common Gotchas and Pitfalls
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Fortran 2023 (ISO/IEC 1539-1:2023, J3/23-007r1) and was checked on 2026-10-19. Every
program below was compiled with gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all (GNU Fortran 16.2)
and run, and, where the feature is portable, built and run with LLVM Flang 23 (flang-23). Both ran in a Linux (aarch64)
container, and the output and diagnostics shown are the real ones. Programs that deliberately fall into a trap say so, show
the real diagnostic, and are followed by the corrected version. Where the two compilers disagree, the page says so, because
a trap that one compiler hides is the most dangerous kind.
Fortran is a small, regular language, but it has a number of rules that behave differently from C, Python or Java, and a handful of defaults that date from the 1950s. The twelve traps below account for most of the surprises that a programmer meets in the first months. Each section follows the same pattern: the program that falls into the trap, what it prints, why (with the clause of the standard), and the flag, run-time check or coding habit that catches it. The traps are drawn from the heatflow running example (a grid of temperatures relaxed by Jacobi or FTCS sweeps), so the code resembles what is written in practice. The compiler flags and sanitizing options are described in Compilers and Compiler Options, the debugging tools in Testing, Debugging and Profiling, and the habits that avoid the traps in the first place in Best Practices and Style.
The traps at a glance
| Trap | What happens | What catches it | Section |
|---|---|---|---|
An initialized local variable keeps its value between calls |
|
code review; |
|
|
|
gfortran |
|
Integer division truncates toward zero |
|
gfortran |
|
|
|
gfortran |
|
|
an |
none; the program crashes or silently loses data |
|
Array sections passed to explicit-shape dummies are copied |
a hidden temporary per call; |
gfortran |
|
Aliased arguments break the optimizer’s assumptions |
|
gfortran warns for the plain case only |
|
Character comparison pads with blanks |
|
none; read the rule |
|
Dummy arguments start at index 1 |
an assumed-shape dummy loses a |
none; print |
|
Uninitialized variables hold garbage |
a local that was never assigned is used |
gfortran |
|
Unit numbers are global |
two parts of a program use unit 10, or a |
|
|
List-directed output ( |
widths and digits differ between compilers |
explicit formats |
|
A big automatic array overflows the stack |
a segmentation fault on entry to the procedure |
|
Implicit save on initialized locals
A variable that is explicitly initialized in its declaration implicitly has the save attribute (8.4: "Explicit
initialization of a variable that is not in a common block implies the SAVE attribute"). In C the same declaration, int count = 0;,
resets the variable at every call; in Fortran the initialization happens once, before the program starts, and the value
survives from call to call. The compiler does not warn, because the program is correct, it just does not do what a C
programmer expects.
module sweep_counter
use, intrinsic :: iso_fortran_env, only: int32
implicit none
private
public :: next_id_wrong, next_id_right
contains
integer(int32) function next_id_wrong() result(id)
integer(int32) :: count = 0 ! initialization implies save
count = count + 1
id = count
end function next_id_wrong
integer(int32) function next_id_right() result(id)
integer(int32) :: count ! a fresh local at every call
count = 0
count = count + 1
id = count
end function next_id_right
end module sweep_counter
program implicit_save
use sweep_counter, only: next_id_wrong, next_id_right
implicit none
integer :: k
do k = 1, 3
print '(a, i0, a, i0)', 'call ', k, ': wrong = ', next_id_wrong()
end do
do k = 1, 3
print '(a, i0, a, i0)', 'call ', k, ': right = ', next_id_right()
end do
end program implicit_save
call 1: wrong = 1
call 2: wrong = 2
call 3: wrong = 3
call 1: right = 1
call 2: right = 1
call 3: right = 1
Both compilers print the same. The corrected function declares the variable and assigns it in an executable statement, which
runs at every call. The consequences go beyond counters: a procedure with a saved local is not safe to call from two threads
or do concurrent iterations at once, and it cannot be pure, because a local variable of a pure subprogram shall not have
the save attribute and initialization implies it (C1598 and its note, 15.7). Variables of the main program, modules and
submodules are saved anyway (8.5.16). Default initialization of a derived-type variable is different: the components get
their default values each time the variable is created, and the standard says that default initialization, unlike
explicit initialization, does not imply save (7.5.4.6). Saved storage is also behind a different surprise: GNU Fortran
moves a large fixed-size local array to static storage, which makes the procedure non-reentrant, and -frecursive (implied
by -fopenmp) changes that choice (see Stack overflow from large automatic arrays). See
Variables, Declarations and Attributes for
the full rules on initialization and save.
Default real and literal kinds
A literal constant without a kind suffix has the default kind: 0.1 is a default real (single precision on every
mainstream compiler), 1.0d0 is a double precision, and 0.1_dp has the kind dp. Assigning a default real to a
real(dp) variable converts the single-precision value, which has already lost the digits, so the variable holds
0.100000001490116… instead of 0.1.
program real_literals
use, intrinsic :: iso_fortran_env, only: real32, real64
implicit none
integer, parameter :: dp = real64
real(dp) :: a, b, c, d
a = 0.1 ! a default real constant, converted to real(dp)
b = 0.1_dp ! a real(dp) constant
c = real(0.1, dp) ! the same trap, spelled with a conversion
d = 1.0d-1 ! the double precision exponent letter
print '(a, es24.16)', 'a = 0.1 : ', a
print '(a, es24.16)', 'b = 0.1_dp : ', b
print '(a, es24.16)', 'c = real(0.1) : ', c
print '(a, es24.16)', 'd = 1.0d-1 : ', d
print '(a, l1)', 'a == b: ', a == b
print '(a, l1)', 'b == d: ', b == d
print '(a, i0, a, i0)', 'kind of 1.0 is ', kind(1.0), ', kind of 1.0d0 is ', kind(1.0d0)
print '(a, i0)', 'real32 kind is ', real32
end program real_literals
a = 0.1 : 1.0000000149011612E-01
b = 0.1_dp : 1.0000000000000001E-01
c = real(0.1) : 1.0000000149011612E-01
d = 1.0d-1 : 1.0000000000000001E-01
a == b: F
b == d: T
kind of 1.0 is 4, kind of 1.0d0 is 8
real32 kind is 4
The two compilers print the same values and diagnose different things. GNU Fortran, under -Wextra, warns about the
comparisons (-Wcompare-reals), which are a separate trap (a comparison of two reals with == is true only when the values
are identical):
g02_real_literals.f90:14:30:
14 | print '(a, l1)', 'a == b: ', a == b
| 1
Warning: Equality comparison for REAL(8) at (1) [-Wcompare-reals]
It did not warn about a = 0.1 under -Wall -Wextra. Adding -Wconversion-extra reports it (Warning: Conversion from
'REAL(4)' to 'REAL(8)' at (1) [-Wconversion-extra]), and LLVM Flang 23 warns about it by default, naming the inexact value:
g02_real_literals.f90:6:7: warning: Default real literal in REAL(8) context might need a kind suffix, as its rounded value 1.00000001490116119384765625e-1_4 is inexact [-Wreal-constant-widening]
a = 0.1 ! a default real constant, converted to real(dp)
^^^
The habits that avoid the trap: give every real literal in double-precision code a kind (0.1_dp, 2.5e-6_dp, never 0.1
or 0.1d0 unless dp is double precision); write the kinds in one module (heatflow_kinds in the examples) so that a
change of precision is a one-line edit; and use real(x, dp), not real(x) or dble(x), when converting (real(0.1, dp)
in the program converts the already-rounded single value, as the output shows). The option -fdefault-real-8 makes every
default real an 8-byte real, but it changes the meaning of the whole program (including calls into libraries that expect
default reals), so it is a porting tool for legacy code, not a fix. Details on kinds,
literals and the numeric models are in
Intrinsic Types and Kinds and
Floating-Point and IEEE Arithmetic.
Integer division
The quotient of two integers is an integer, truncated toward zero (10.1.5.2.2). A mixed expression such as lx * (1 / (nx - 1)) evaluates the
integer division first, so a spacing computed this way is 0 for every grid larger than 2 points. The remainder functions
disagree for negative operands: mod takes the sign of the first argument, modulo the sign of the second.
program integer_division
use, intrinsic :: iso_fortran_env, only: real64, int32
implicit none
integer, parameter :: dp = real64
integer(int32) :: nx, big
real(dp) :: lx, dx_wrong, dx_right
nx = 11
lx = 1.0_dp
dx_wrong = lx * (1 / (nx - 1)) ! integer division first: 1 / 10 is 0
dx_right = lx / real(nx - 1, dp)
print '(a, f6.3, a, f6.3)', 'dx wrong = ', dx_wrong, ', dx right = ', dx_right
print '(a, 4(i3))', '7/2, -7/2, 7/(-2), 1/2: ', 7/2, -7/2, 7/(-2), 1/2
print '(a, 2(i3))', 'mod(-7,3), modulo(-7,3): ', mod(-7, 3), modulo(-7, 3)
print '(a, 2(f6.2))', 'nint(2.5), 2.5 truncated: ', real(nint(2.5)), real(int(2.5))
big = huge(big)
big = big + nx ! overflow: wraps around, no diagnostic
print '(a, i0)', 'huge + 11 = ', big
end program integer_division
dx wrong = 0.000, dx right = 0.100
7/2, -7/2, 7/(-2), 1/2: 3 -3 -3 0
mod(-7,3), modulo(-7,3): -1 2
nint(2.5), 2.5 truncated: 3.00 2.00
huge + 11 = -2147483638
GNU Fortran adds two warnings at compile time, both about constants (-Winteger-division, enabled by -Wall):
g03_integer_division.f90:12:71:
12 | print '(a, 4(i3))', '7/2, -7/2, 7/(-2), 1/2: ', 7/2, -7/2, 7/(-2), 1/2
| 1
Warning: Integer division truncated to constant '0' at (1) [-Winteger-division]
g03_integer_division.f90:12:63:
12 | print '(a, 4(i3))', '7/2, -7/2, 7/(-2), 1/2: ', 7/2, -7/2, 7/(-2), 1/2
| (same warning, quotient -3)
Warning: Integer division truncated to constant '-3' at (1) [-Winteger-division]
The division with variables (1 / (nx - 1)) is not reported by either compiler, and neither is the last line of the
output: huge(big) + nx overflows a 32-bit integer and wraps around to a negative number. The standard prohibits an
operation whose result is not defined by the arithmetic of the processor (10.1.5.2.4) but does not require the processor
to detect it, and -fcheck=all does not. GNU Fortran’s -ftrapv makes the overflow trap: the same program built with it
stopped with Aborted and a backtrace at the addition.
The habits: convert one operand explicitly (real(nx - 1, dp), as in dx_right), or write the constant as a real
(1.0_dp / (nx - 1)), and use nint when rounding, not int, which truncates (int(2.5) is 2, nint(2.5) is 3). For
index arithmetic that needs a floor, modulo and floor are the right functions for negative numbers. See
Expressions and Assignment for the evaluation rules.
No short-circuit evaluation
The standard does not require a .and. b to skip b when a is false. A processor may evaluate both operands, either one first,
or neither if it can find the value otherwise: once the interpretation of a logical operation is established, the processor may
evaluate any other expression that is logically equivalent (10.1.5.4.2). The conditions that depend on each other, such as a bounds test and a subscript, are therefore a bug waiting
for a different compiler or a different optimization level.
program short_circuit
use, intrinsic :: iso_fortran_env, only: real64
implicit none
integer, parameter :: dp = real64
real(dp) :: t(3) = [20.0_dp, 35.5_dp, 12.0_dp]
integer :: i, n
n = size(t)
! Search for the first temperature above 30 -- written as in C or Python.
i = 1
do while (i <= n .and. t(i) <= 30.0_dp) ! t(n + 1) may be evaluated
i = i + 1
end do
print '(a, i0)', 'first hot cell: ', i
! The same search for a limit that is never reached.
i = 1
do while (i <= n .and. t(i) <= 100.0_dp)
i = i + 1
end do
print '(a, i0)', 'first cell above 100: ', i
end program short_circuit
GNU Fortran with -fcheck=all evaluates the second operand for i = 4 and stops with a run-time error (the output of
the first search is printed first):
first hot cell: 2
At line 17 of file g04_short_circuit.f90
Fortran runtime error: Index '4' of dimension 1 of array 't' above upper bound of 3
Error termination. Backtrace:
#0 0xffff82f4196f in ???
...
LLVM Flang 23 without run-time checks reads one element past the array (an out-of-bounds read, which is undefined behaviour) and prints a plausible answer:
first hot cell: 2
first cell above 100: 4
That second result is the worst outcome: the program "works". The fix is to structure the test so that the dependent operand is only reached when it is valid:
program short_circuit_fixed
use, intrinsic :: iso_fortran_env, only: real64
implicit none
integer, parameter :: dp = real64
real(dp) :: t(3) = [20.0_dp, 35.5_dp, 12.0_dp]
integer :: i, n
logical :: hot
n = size(t)
! Option 1: a loop with an explicit exit; the test of t(i) is only reached if i <= n.
do i = 1, n
if (t(i) > 100.0_dp) exit
end do
print '(a, i0)', 'first cell above 100 (exit loop): ', i
! Option 2: the intrinsic does the search.
i = findloc(t > 100.0_dp, .true., dim=1)
print '(a, i0)', 'first cell above 100 (findloc, 0 if none): ', i
! Option 3: a nested if guards a single test.
i = n + 1
hot = .false.
if (i <= n) then
hot = t(i) > 100.0_dp
end if
print '(a, l1)', 'nested if, i = n + 1: ', hot
! Option 4: the Fortran 2023 conditional expression evaluates only the chosen branch.
hot = (i <= n ? t(i) > 100.0_dp : .false.)
print '(a, l1)', 'conditional expression, i = n + 1: ', hot
end program short_circuit_fixed
first cell above 100 (exit loop): 4
first cell above 100 (findloc, 0 if none): 0
nested if, i = n + 1: F
conditional expression, i = n + 1: F
Both compilers print the same. The exit loop is the idiom to prefer, findloc is the idiom for a pure search (it returns
0 when nothing matches), and a nested if is the idiom for a single guarded test. The Fortran 2023 conditional expression
(cond ? a : b) evaluates only the chosen branch (10.1.4, NOTE 3), which .and. and merge do not promise, and both
compilers accepted it in the last test; it is described in
Expressions and Assignment. The same hazard applies to
present(x) .and. x > 0 with an optional argument, which must be written as two nested tests
(Procedures, Functions and Subroutines).
intent(out) deallocates and undefines
intent(out) is not "write only, keep the rest": it states that the incoming value is not used, and the language enforces
that. On entry to the procedure (8.5.10 and 9.7.3.2):
-
an allocated
allocatableactual argument that corresponds to anintent(out)allocatable dummy is deallocated; -
a nonpointer dummy argument becomes undefined, except for subcomponents with default initialization, which get their default value back;
-
a pointer dummy argument has an undefined association status.
module readings
use, intrinsic :: iso_fortran_env, only: real64, int32
implicit none
private
public :: probe_t, append_wrong, append_right, reset_probe, set_edges
integer, parameter :: dp = real64
type :: probe_t
integer(int32) :: count = 0 ! default initialization
real(dp) :: last = -1.0_dp
end type probe_t
contains
! Intended to add a value to the end of buf -- but intent(out) deallocates buf on entry.
subroutine append_wrong(buf, value)
real(dp), allocatable, intent(out) :: buf(:)
real(dp), intent(in) :: value
buf = [buf, value] ! buf is no longer allocated here
end subroutine append_wrong
subroutine append_right(buf, value)
real(dp), allocatable, intent(inout) :: buf(:)
real(dp), intent(in) :: value
buf = [buf, value]
end subroutine append_right
! intent(out) re-applies the default initialization: count is reset to 0.
subroutine reset_probe(p, value)
type(probe_t), intent(out) :: p
real(dp), intent(in) :: value
p%last = value ! count was not mentioned -- it is 0 again
end subroutine reset_probe
! An intent(out) array that is only partly assigned: the interior is undefined.
subroutine set_edges(u)
real(dp), intent(out) :: u(:)
u(1) = 0.0_dp
u(size(u)) = 1.0_dp
end subroutine set_edges
end module readings
program intent_out
use, intrinsic :: iso_fortran_env, only: real64
use readings
implicit none
integer, parameter :: dp = real64
real(dp), allocatable :: buf(:)
type(probe_t) :: p
buf = [1.0_dp, 2.0_dp]
call append_right(buf, 3.0_dp)
print '(a, *(f5.1))', 'append_right: ', buf
p = probe_t(count=5, last=2.5_dp)
call reset_probe(p, 9.0_dp)
print '(a, i0, a, f4.1)', 'after reset_probe: count = ', p%count, ', last = ', p%last
call append_wrong(buf, 4.0_dp)
print '(a, *(f5.1))', 'append_wrong:', buf
end program intent_out
The two correct calls print what a programmer expects, and the last one is where the trap is:
append_right: 1.0 2.0 3.0
after reset_probe: count = 0, last = 9.0
reset_probe shows the second rule: the caller’s count = 5 is gone, because probe_t has default initialization and
intent(out) re-applies it. The last call, append_wrong, expects buf to still hold [1, 2, 3], but the dummy was deallocated on
entry, so [buf, value] refers to an unallocated array. The standard calls this an error, and the compilers disagree on what
they do with it. Under -fcheck=all GNU Fortran 16.2 stops the program with a segmentation fault:
Program received signal SIGSEGV: Segmentation fault - invalid memory reference.
Backtrace for this error:
#0 0xffff9a77196f in ???
...
#3 0x4016a0 in __readings_MOD_append_wrong
at /work/g05_intent_out.f90:16
#4 0x401bfb in intent_out
at /work/g05_intent_out.f90:53
Segmentation fault
whereas LLVM Flang 23 prints append_wrong: 4.0, silently treating the unallocated array as empty. The fix is
intent(inout) (as in append_right), or, for a routine that really replaces its argument, the result is built in a local
and moved with move_alloc. The same applies to arrays: an intent(out) array that is only partly assigned, such as
set_edges, leaves the interior undefined, and reading it is a use of an undefined value. Use intent(inout) whenever the
procedure reads the incoming value, even once. See
Allocatable Arrays and Dynamic Memory and
Procedures, Functions and Subroutines.
Array temporaries and aliasing violations
Hidden copies
An actual argument that is not contiguous (a row of a column-major array, or a(::2)) cannot be passed by address to
a dummy argument that requires contiguous data: an explicit-shape (x(n)), assumed-size (x(*)) or contiguous dummy,
or a dummy with the old implicit interface. The compiler creates a temporary, copies the data in before the call and, for
intent(out) or intent(inout), copies it back after. The result is correct, but the cost is a hidden allocation and copy at
every call, which in a hot loop is a large slowdown. An assumed-shape dummy (x(:)) receives a descriptor with the stride and
needs no copy.
module norms
use, intrinsic :: iso_fortran_env, only: real64
implicit none
private
public :: sum_explicit, sum_assumed
integer, parameter :: dp = real64
contains
real(dp) function sum_explicit(n, x) result(s) ! explicit-shape dummy: needs contiguous data
integer, intent(in) :: n
real(dp), intent(in) :: x(n)
s = sum(x)
end function sum_explicit
real(dp) function sum_assumed(x) result(s) ! assumed-shape dummy: any stride is fine
real(dp), intent(in) :: x(:)
s = sum(x)
end function sum_assumed
end module norms
program array_temps
use, intrinsic :: iso_fortran_env, only: real64
use norms
implicit none
integer, parameter :: dp = real64
real(dp) :: u(4, 3)
integer :: i, j
do j = 1, 3
do i = 1, 4
u(i, j) = 10.0_dp * j + i
end do
end do
print '(a, f6.1)', 'column u(:, 2), contiguous, explicit-shape: ', sum_explicit(4, u(:, 2))
print '(a, f6.1)', 'row u(2, :), strided, assumed-shape: ', sum_assumed(u(2, :))
print '(a, f6.1)', 'row u(2, :), strided, explicit-shape: ', sum_explicit(3, u(2, :))
end program array_temps
column u(:, 2), contiguous, explicit-shape: 90.0
row u(2, :), strided, assumed-shape: 66.0
row u(2, :), strided, explicit-shape: 66.0
Both compilers print the same result. The third call needs a temporary, and GNU Fortran can tell you, at compile time with
-Warray-temporaries and at run time with -fcheck=array-temps (part of -fcheck=all, and the reason the strict build
prints a run-time warning on standard error):
g07_array_temps.f90:34:85:
34 | print '(a, f6.1)', 'row u(2, :), strided, explicit-shape: ', sum_explicit(3, u(2, :))
| 1
Warning: Creating array temporary at (1) for argument 'x' [-Warray-temporaries]
...
At line 34 of file g07_array_temps.f90
Fortran runtime warning: An array temporary was created for argument 'x' of procedure 'sum_explicit'
LLVM Flang 23 printed no diagnostic. The cure is an assumed-shape dummy wherever the array may be a section, and
contiguous only when the copy is acceptable. The rules for each kind of dummy array are in
Array Arguments: Assumed Shape, Size and Rank,
and the performance effect in
Performance and Memory Layout.
Aliasing
If a procedure defines a dummy argument, that dummy argument may not be associated with any other dummy argument or accessible variable that is referenced in the procedure (15.5.2.14). The compiler relies on it: it keeps values in registers and reorders loads and stores. If the caller breaks the promise, the answer depends on the optimization level.
module smoothing
use, intrinsic :: iso_fortran_env, only: real64
implicit none
private
public :: smooth
integer, parameter :: dp = real64
contains
! y(i) = average of x(i-1), x(i), x(i+1). x and y must not overlap: y is defined
! while x is still being read, and the compiler may assume they are different.
subroutine smooth(x, y)
real(dp), intent(in) :: x(:)
real(dp), intent(out) :: y(:)
integer :: i
y(1) = x(1)
y(size(x)) = x(size(x))
do i = 2, size(x) - 1
y(i) = (x(i - 1) + x(i) + x(i + 1)) / 3.0_dp
end do
end subroutine smooth
end module smoothing
program aliasing
use, intrinsic :: iso_fortran_env, only: real64
use smoothing, only: smooth
implicit none
integer, parameter :: dp = real64
real(dp) :: a(6), b(6)
a = [0.0_dp, 0.0_dp, 9.0_dp, 0.0_dp, 0.0_dp, 0.0_dp]
call smooth(a, b) ! conforming: separate arrays
print '(a, *(f6.2))', 'separate arrays: ', b
call smooth(a, a) ! not conforming: x and y are the same array
print '(a, *(f6.2))', 'aliased call: ', a
! Array assignment is safe even when both sides overlap: the right side is evaluated first.
a = [1.0_dp, 2.0_dp, 3.0_dp, 4.0_dp, 5.0_dp, 6.0_dp]
a(2:6) = a(1:5)
print '(a, *(f6.2))', 'a(2:6) = a(1:5): ', a
end program aliasing
separate arrays: 0.00 3.00 3.00 3.00 0.00 0.00
aliased call: 0.00 3.00 4.00 1.33 0.44 0.00
a(2:6) = a(1:5): 1.00 1.00 2.00 3.00 4.00 5.00
With the strict flags (-O0) the aliased call reads values that were just overwritten, which is what a sequential
reading of the loop would give. GNU Fortran detects the literal case at compile time:
g06_aliasing.f90:31:14:
31 | call smooth(a, a) ! not conforming: x and y are the same array
| 1
Warning: Same actual argument associated with INTENT(IN) argument 'x' and INTENT(OUT) argument 'y' at (1)
and when the same file is compiled at different optimization levels the line "aliased call" changes:
-O0
aliased call: 0.00 3.00 4.00 1.33 0.44 0.00
-O2
aliased call: 0.00 3.00 3.00 3.00 0.00 0.00
-O3
aliased call: 0.00 3.00 3.00 3.00 0.00 0.00
At -O2 and -O3 the optimizer assumed that x and y do not overlap, loaded the three inputs before storing, and produced the
same numbers as the conforming call. Neither answer is "right"; the program is not conforming, so both are acceptable. LLVM Flang 23
printed the -O0-style result. The aliasing is usually not a literal call f(a, a) but two overlapping sections (a(1:5) and
a(2:6)) or a pointer and its target, which no compiler warns about. The last assignment shows what does work: a
whole-array assignment such as a(2:6) = a(1:5) is defined as if the right side were evaluated completely before the
assignment (10.2.1.3), so overlapping operands are safe there, and the compiler inserts a temporary or reverses the loop if needed. A do
loop that does the same in place would have to be written backwards by the programmer.
Character padding in comparisons and assignments
In a comparison, the shorter of two character operands is treated as if extended on the right with blanks to the length of the
longer (10.1.5.5.1). Trailing blanks are therefore never significant in ==, /=, < and the other relational operators. An
assignment to a longer variable pads with blanks, and to a shorter variable it silently truncates (10.2.1.3). A
character(len=8) variable always contains 8 characters, and len_trim is the length without the blanks.
program character_padding
implicit none
character(len=8) :: name
character(len=3) :: short
character(len=:), allocatable :: dyn
name = 'jacobi' ! padded with blanks to 8 characters
print '(a, l1)', "name == 'jacobi' : ", name == 'jacobi'
print '(a, l1)', "name == 'jacobi ' : ", name == 'jacobi '
print '(a, l1)', "'ab' == 'ab ' : ", 'ab' == 'ab '
print '(a, l1)', "'ab' < 'ab' // char(1) : ", 'ab' < 'ab' // char(1)
print '(a, l1)', "'ab ' == 'ab' // char(0) : ", 'ab ' == 'ab' // char(0)
print '(a, i0, a, i0)', 'len(name) = ', len(name), ', len_trim(name) = ', len_trim(name)
print '(a, i0)', "index(name, ' ') = ", index(name, ' ')
short = 'ftcs' ! silently truncated to 3 characters
print '(3a)', "short = '", short, "'"
dyn = name ! allocatable length takes the padded length
print '(a, i0)', 'len(dyn) = ', len(dyn)
dyn = trim(name)
print '(a, i0)', 'len(dyn) after trim = ', len(dyn)
print '(3a)', "[", name, "]"
print '(3a)', "[", trim(name), "]"
end program character_padding
name == 'jacobi' : T
name == 'jacobi ' : T
'ab' == 'ab ' : T
'ab' < 'ab' // char(1) : F
'ab ' == 'ab' // char(0) : F
len(name) = 8, len_trim(name) = 6
index(name, ' ') = 7
short = 'ftc'
len(dyn) = 8
len(dyn) after trim = 6
[jacobi ]
[jacobi]
Both compilers print the same. What this shows:
-
name == 'jacobi'is true, although the strings have lengths 8 and 6, and'ab' == 'ab 'is true for the same reason. -
The padding blank is a real character in the collating sequence, so
'ab' < 'ab' // char(1)is false: comparing'ab'with a longer string pads the shorter one with a blank (code 32), which is greater thanchar(1). The ordering of strings with different lengths is therefore the ordering of the padded strings, not a lexicographic order with "shorter is smaller". -
index(name, ' ')finds the first padding blank, not the end of the name, andlen(name)is the declared length. Uselen_trimandtrim. -
Assigning
'ftcs'to acharacter(len=3)variable keeps'ftc'without a run-time error; GNU Fortran warns at compile time when it can see it (-Wcharacter-truncation, enabled by-Wall):
g08_character_padding.f90:14:79:
14 | short = 'ftcs' ! silently truncated to 3 characters
| 1
Warning: CHARACTER expression will be truncated in assignment (3/4) at (1) [-Wcharacter-truncation]
-
A
character(len=:), allocatablevariable takes the length of the assigned expression, sodyn = namekeeps the padding (length 8) whiledyn = trim(name)has length 6. The output[jacobi ]versus[jacobi]is the visible effect.
The habit is to use trim when building strings and file names, adjustl when aligning text, and the character(len=:),
allocatable variables for text of unknown length. The details are in
Characters and Strings.
Lower bounds of dummy arguments
A Fortran array can have any lower bound, and a common use is a halo: u(0:nx+1) with the boundary values at 0 and nx+1.
Whether a dummy argument sees the bounds depends on how it is declared (8.5.8):
module bounds_demo
use, intrinsic :: iso_fortran_env, only: real64
implicit none
private
public :: show
integer, parameter :: dp = real64
contains
subroutine show(label, shape_info)
character(*), intent(in) :: label
integer, intent(in) :: shape_info(2)
print '(a, a, i0, a, i0)', label, ': lbound = ', shape_info(1), ', ubound = ', shape_info(2)
end subroutine show
end module bounds_demo
module bounds_probes
use, intrinsic :: iso_fortran_env, only: real64
implicit none
private
public :: assumed_shape, assumed_shape_lb, explicit_shape, allocatable_dummy, pointer_dummy
integer, parameter :: dp = real64
contains
subroutine assumed_shape(a, lb, ub)
real(dp), intent(in) :: a(:)
integer, intent(out) :: lb, ub
lb = lbound(a, 1); ub = ubound(a, 1)
end subroutine assumed_shape
subroutine assumed_shape_lb(a, lb, ub)
real(dp), intent(in) :: a(0:)
integer, intent(out) :: lb, ub
lb = lbound(a, 1); ub = ubound(a, 1)
end subroutine assumed_shape_lb
subroutine explicit_shape(a, n, lb, ub)
integer, intent(in) :: n
real(dp), intent(in) :: a(0:n - 1)
integer, intent(out) :: lb, ub
lb = lbound(a, 1); ub = ubound(a, 1)
end subroutine explicit_shape
subroutine allocatable_dummy(a, lb, ub)
real(dp), allocatable, intent(in) :: a(:)
integer, intent(out) :: lb, ub
lb = lbound(a, 1); ub = ubound(a, 1)
end subroutine allocatable_dummy
subroutine pointer_dummy(a, lb, ub)
real(dp), pointer, intent(in) :: a(:)
integer, intent(out) :: lb, ub
lb = lbound(a, 1); ub = ubound(a, 1)
end subroutine pointer_dummy
end module bounds_probes
program lower_bounds
use, intrinsic :: iso_fortran_env, only: real64
use bounds_demo, only: show
use bounds_probes
implicit none
integer, parameter :: dp = real64
real(dp), allocatable, target :: u(:)
real(dp), pointer :: p(:)
integer :: lb, ub
allocate (u(0:5)) ! halo-style bounds 0:5
u = 1.0_dp
p => u
print '(a, i0, a, i0)', 'actual argument: lbound = ', lbound(u, 1), ', ubound = ', ubound(u, 1)
call assumed_shape(u, lb, ub); call show('assumed shape a(:) ', [lb, ub])
call assumed_shape_lb(u, lb, ub); call show('assumed shape a(0:) ', [lb, ub])
call explicit_shape(u, 6, lb, ub); call show('explicit shape a(0:n-1)', [lb, ub])
call allocatable_dummy(u, lb, ub); call show('allocatable a(:) ', [lb, ub])
call pointer_dummy(p, lb, ub); call show('pointer a(:) ', [lb, ub])
call assumed_shape(u(2:4), lb, ub); call show('section u(2:4) to a(:)', [lb, ub])
end program lower_bounds
actual argument: lbound = 0, ubound = 5
assumed shape a(:) : lbound = 1, ubound = 6
assumed shape a(0:) : lbound = 0, ubound = 5
explicit shape a(0:n-1): lbound = 0, ubound = 5
allocatable a(:) : lbound = 0, ubound = 5
pointer a(:) : lbound = 0, ubound = 5
section u(2:4) to a(:): lbound = 1, ubound = 3
Both compilers print the same. The rule is:
| Dummy argument | Bounds seen in the procedure |
|---|---|
assumed shape, |
lower bound 1, upper bound the extent (8.5.8.3), whatever the bounds of the actual argument |
assumed shape with a lower bound, |
the stated lower bound, the extent of the actual argument |
explicit shape, |
the declared bounds |
|
the bounds of the actual argument (the dummy is the argument itself) |
an array section, |
a new array: lower bound 1 |
The trap is the first row: a routine written for u(0:nx+1) that receives it through an assumed-shape dummy sees
u(1:nx+2), and its u(0) is out of bounds. The result is an off-by-one error that shows only at the edges. The fix is to
declare the lower bound in the dummy (real(dp), intent(inout) :: u(0:)) or to receive the array as allocatable
or pointer. The result of an array expression or of an intrinsic such as pack or reshape also starts at 1. A loop that must accept
any array should run from lbound(u, 1) to ubound(u, 1), not from the constant 1.
See Array Arguments: Assumed Shape, Size and Rank and
Arrays Fundamentals.
Uninitialized variables
A local variable without an initializer has an undefined value until it is assigned (19.6.4). Reading it gives whatever was in memory, and the program may print a plausible number, as the one below does.
program uninitialized
use, intrinsic :: iso_fortran_env, only: real64
implicit none
integer, parameter :: dp = real64
real(dp) :: residual, peak
real(dp) :: u(5)
integer :: i, steps
u(1:4) = [1.0_dp, 4.0_dp, 2.0_dp, 3.0_dp] ! u(5) is never set
do i = 1, 4
if (u(i) > peak) peak = u(i) ! peak is never initialized
end do
steps = steps + 1 ! nor is steps
residual = maxval(abs(u))
print '(a, es12.4)', 'peak = ', peak
print '(a, i0)', 'steps = ', steps
print '(a, es12.4)', 'residual = ', residual
end program uninitialized
peak = 4.0000E+00
steps = 1
residual = 4.0000E+00
Flang printed the same three lines. They are plausible, and they come out the same each time on this machine, which is why
this trap survives testing: steps happened to start as zero and peak as a value below 4, so the program "works". It
printed steps = -1624492493 when compiled with -O1. The tools that catch it are:
-
the compile-time analysis of GNU Fortran. In the strict build above it reported, once the optimizer’s data flow was available:
g10_uninitialized.f90:12:64: 12 | steps = steps + 1 ! nor is steps | ^ Warning: 'steps' may be used uninitialized [-Wmaybe-uninitialized]At
-O1the warning is the stronger'steps' is used uninitialized [-Wuninitialized]. The analysis is conservative and did not reportpeak, which is only uninitialized on a path through the loop. -
-finit-real=snan -finit-integer=nwith-ffpe-trap=invalid(GNU Fortran): every uninitialized localrealstarts as a signaling NaN, so the first arithmetic use stops the program at the line:Floating point exception Program received signal SIGFPE: Floating-point exception - erroneous arithmetic operation. Backtrace for this error: ... #3 0x4009c4 in uninitialized at /work/g10_uninitialized.f90:10Line 10 is the comparison with
peak. The-finit-*options exist for finding bugs, not for fixing them; the program must assign every variable before use. They are not available in Flang 23 (see Compilers and Compiler Options). -
Valgrind’s Memcheck on a plain
-gbuild (no optimization), which reported two uses of undefined values: the comparison withpeakat line 10 and themaxval(abs(u))that reads the never-setu(5)at line 13:==49== Conditional jump or move depends on uninitialised value(s) ==49== at 0x400934: MAIN__ (g10_uninitialized.f90:10) ==49== by 0x400B87: main (g10_uninitialized.f90:17) ==49== Conditional jump or move depends on uninitialised value(s) ==49== at 0x4009E0: MAIN__ (g10_uninitialized.f90:13) ==49== by 0x400B87: main (g10_uninitialized.f90:17)
The fixes: initialize in the declaration only when save semantics are acceptable (see Implicit save on initialized locals),
otherwise assign on the first line of the procedure; initialize allocatable arrays at the allocate with source= or mold=
and a value.
Unit numbers already in use
A unit number is a global resource of the program. The units 5, 6 and 0 are preconnected on common compilers to standard
input, output and error, and iso_fortran_env provides them under the names input_unit, output_unit and error_unit
(error_unit is not required to be 0). Beyond that, the
number is the programmer’s choice, and libraries that choose a fixed number (10, 11, 20, …) collide with the program.
In a read or write, unit=* means the first two of these (12.5.5).
program units
use, intrinsic :: iso_fortran_env, only: input_unit, output_unit, error_unit
implicit none
integer :: u1, u2, ios
character(len=256) :: msg, name
logical :: is_open, exists
print '(a, 3(1x, i0))', 'preconnected units (input, output, error):', input_unit, output_unit, error_unit
! A hard-coded unit number is easy to reuse by accident. OPEN of a connected unit
! on a different file is not an error: the first file is closed without a word.
open (unit=10, file='first.txt', status='replace', action='write')
write (10, '(a)') 'first'
open (unit=10, file='second.txt', status='replace', action='write')
write (10, '(a)') 'second'
inquire (unit=10, name=name)
print '(a, a)', 'unit 10 is now connected to ', trim(name)
close (10)
! Writing to a unit that was never opened creates a file with a made-up name.
write (77, '(a)') 'surprise'
close (77)
inquire (file='fort.77', exist=exists)
print '(a, l1)', 'file fort.77 exists: ', exists
! status='new' fails when the file exists; iostat and iomsg report it.
msg = ''
open (unit=11, file='first.txt', status='new', iostat=ios, iomsg=msg)
print '(a, l1, a, l1)', 'open status=new failed: ', ios /= 0, ', message given: ', len_trim(msg) > 0
! newunit= picks a free negative number, so it cannot collide.
open (newunit=u1, file='first.txt', status='old', action='read')
open (newunit=u2, file='second.txt', status='old', action='read')
print '(a, l1)', 'newunit values are negative: ', u1 < 0 .and. u2 < 0
inquire (unit=10, opened=is_open)
print '(a, l1)', 'unit 10 still open: ', is_open
close (u1, status='delete')
close (u2, status='delete')
open (newunit=u1, file='fort.77', status='old')
close (u1, status='delete')
end program units
preconnected units (input, output, error): 5 6 0
unit 10 is now connected to second.txt
file fort.77 exists: T
open status=new failed: T, message given: T
newunit values are negative: T
unit 10 still open: F
Both compilers print the same. What happened:
-
The second
open(unit=10, file='second.txt')was accepted. The standard says that connecting a connected unit to a different file first closes the first file as if byclose(12.5.4), so no error is raised and the first file is quietly closed. If the first file was still in use through that unit, thewritegoes into the wrong file. The page on input and output shows the related case of two units on the same file, which the two compilers handle differently (Input/Output: Units and Files). -
write (77, ...)on a unit that was never opened created a file namedfort.77. The standard says that an I/O statement (other thanopen,close,inquireandwait) shall not refer to a unit that does not exist (12.5.3), and that the set of units that exist is processor dependent; both compilers treat every number as existing and connect it tofort.N, so a typo in the unit number produces a stray file, not an error. The program removes the file again at the end. -
open(..., status='new')on an existing file failed, andiostat=andiomsg=captured it; withoutiostatthe program would have stopped. -
open(newunit=u)(Fortran 2008) picks a unit that is not in use and negative, so it cannot collide with a user-chosen number. This is what library code should always use.
inquire(unit=n, opened=flag) tests a number, and numbers 0 to 9 are best left alone, because several compilers preconnect some of
them. iostat= and iomsg= on every open, read and write test an I/O operation without stopping the program, as explained in
Error Handling and Program Termination.
List-directed output formatting
print *, x and write (*, *) x use list-directed formatting (13.10.4): the processor chooses the layout. The standard
fixes only a few things (a leading blank for carriage control, values separated by a blank or a comma, a character value
without quotes unless it is a namelist). The width, the number of digits and whether a value like 0.1 is written as
0.1 or 0.10000000000000001 are the compiler’s choice.
program list_directed
use, intrinsic :: iso_fortran_env, only: real32, real64, int32
implicit none
real(real64) :: t = 0.1_real64, big = 1.0e9_real64, small = 2.5e-6_real64
real(real32) :: s = 0.1_real32
integer(int32) :: n = 42
logical :: ok = .true.
character(len=5) :: word = 'ab'
print *, 'steps =', n, 'ok =', ok
print *, t, s
print *, big, small
print *, 'word=[', word, ']'
print *, cmplx(1.0_real64, -2.0_real64, real64)
print *, [1.0_real64, 2.5_real64, 3.0_real64]
print '(a, i0, a, f8.3)', 'n=', n, ' t=', t
print '(es12.4, 1x, es12.4)', big, small
end program list_directed
steps = 42 ok = T
0.10000000000000001 0.100000001
1000000000.0000000 2.5000000000000002E-006
word=[ab ]
(1.0000000000000000,-2.0000000000000000)
1.0000000000000000 2.5000000000000000 3.0000000000000000
n=42 t= 0.100
1.0000E+09 2.5000E-06
The above is the output of GNU Fortran 16.2. LLVM Flang 23 prints the same two lines at the end, and the rest in its own style:
steps = 42 ok = T
.1 .1
1000000000. 2.5E-06
word=[ab ]
(1.,-2.)
1. 2.5 3.
n=42 t= 0.100
1.0000E+09 2.5000E-06
The two outputs differ in the field widths (steps = 42 against steps = 42), in the number of digits (Flang
writes .1 for both 0.1_real64 and 0.1_real32, GNU Fortran writes 17 significant digits for a real64 and 9 for a real32),
in the exponent style (2.5000000000000002E-006 against 2.5E-06), and in the padding of the complex number and the array. Both are valid. A test that
compares the output of print * with a stored text breaks when the compiler changes. For output that others read or
programs parse, use an explicit format: i0 for integers of any width, f8.3 or es12.4 for reals, a for text,
l1 for logicals, and an unlimited repeat count for lists ((*(f8.3))). For round-tripping reals, es24.16e3 is a usual choice. All the edit descriptors are in
Format Specifications and Edit Descriptors, and list-directed and namelist
input in Internal Files, Namelist and Non-Advancing I/O.
Stack overflow from large automatic arrays
An automatic array is a local array whose bounds depend on a dummy argument or other nonconstant expression (real(dp)
:: work(n)). The standard says nothing about where it lives, and the compilers differ. A fixed-size local array can
overflow the stack too.
module work_arrays
use, intrinsic :: iso_fortran_env, only: real64, int64
implicit none
private
public :: mean_automatic, mean_allocatable
integer, parameter :: dp = real64
contains
! An automatic array: its size is a dummy argument, and it usually lives on the stack.
real(dp) function mean_automatic(n) result(m)
integer(int64), intent(in) :: n
real(dp) :: work(n)
work = 1.0_dp
m = sum(work) / real(n, dp)
end function mean_automatic
! An allocatable local: the data live on the heap, and allocate reports a failure.
real(dp) function mean_allocatable(n) result(m)
integer(int64), intent(in) :: n
real(dp), allocatable :: work(:)
allocate (work(n))
work = 1.0_dp
m = sum(work) / real(n, dp)
end function mean_allocatable
end module work_arrays
program stack_usage
use, intrinsic :: iso_fortran_env, only: int64, real64
use work_arrays
implicit none
integer(int64), parameter :: n = 50000000_int64 ! 400 MB of real64
print '(a, f4.1)', 'allocatable: ', mean_allocatable(n)
print '(a, f4.1)', 'automatic: ', mean_automatic(n)
end program stack_usage
The program works with the automatic array on an 8 MiB stack (ulimit -s is 8192 in the container) when built with
GNU Fortran, because GNU Fortran puts automatic arrays on the heap by default:
allocatable: 1.0
automatic: 1.0
Source: GNU Fortran manual: Options for code generation conventions (-fstack-arrays, -fmax-stack-var-size, -frecursive), LLVM Flang documentation
and crashes in these builds, which end with Segmentation fault (the Flang run showed no output line at all, presumably
because the first line was still in a buffer when the process died):
| Build | Result |
|---|---|
|
|
|
|
|
|
The same program on Flang with ulimit -s 1000000 (about 1 GB) ran and printed both lines, which shows that the cause is the
stack limit, but raising the limit is a workaround that depends on the environment (and on OMP_STACKSIZE for the threads of an OpenMP
program). -fstack-arrays is enabled by -Ofast, so a program that worked at -O2 can start to crash at -Ofast. A second
variant, a fixed-size array in a procedure, behaves differently again:
program fixed_local
use, intrinsic :: iso_fortran_env, only: real64
implicit none
call work()
contains
subroutine work()
real(real64) :: big(50000000) ! fixed size, 400 MB
big = 1.0_real64
print '(a, f4.1)', 'mean = ', sum(big) / size(big)
end subroutine work
end program fixed_local
With GNU Fortran 16.2 the program printed mean = 1.0, because a fixed-size local array larger than the limit of
-fmax-stack-var-size is placed in static memory, which makes the procedure non-reentrant. With -frecursive or
-fopenmp (which forbid that choice), with -fmax-stack-var-size=1000000000, and with Flang 23 it crashed with
Segmentation fault. Neither behaviour
is wrong, and neither is portable.
The cure is an allocatable array for anything whose size is large or comes from the user: allocate(work(n), stat=ierr) reports
a failure instead of crashing, and the memory is released at the end of the scope. The two ways of getting this wrong in the opposite
direction (an array allocated in a hot loop) are covered in
Performance and Memory Layout; the rules for allocatable arrays
and the same experiment in a different setting are in
Allocatable Arrays and Dynamic Memory. The
diagnostic to remember: a segmentation fault on entering a procedure, before any of its statements run, is a stack overflow until proven otherwise.
GNU Fortran has -Wstack-usage=<byte-size> (not tested here) for finding procedures that may use too much stack.
More small traps
Four more traps are small enough for a single program.
program loops_and_reals
use, intrinsic :: iso_fortran_env, only: real64
implicit none
integer, parameter :: dp = real64
real(dp) :: a, b, tol
integer :: i, n
! Loop variable after the loop: one past the last iteration, not the last value.
do i = 1, 5
end do
print '(a, i0)', 'i after do i = 1, 5 : ', i
n = 0
do i = 5, 1 ! zero iterations; i is still set to the initial value
n = n + 1
end do
print '(a, i0, a, i0)', 'trips of do i = 5, 1: ', n, ', i = ', i
! Changing the bounds inside the loop has no effect: they are evaluated once.
n = 3
do i = 1, n
n = 10
end do
print '(a, i0, a, i0)', 'n after loop = ', n, ', i after loop = ', i
! Comparing reals.
a = 0.1_dp + 0.2_dp
b = 0.3_dp
tol = 4 * epsilon(b)
print '(a, l1)', '0.1 + 0.2 == 0.3: ', a == b
print '(a, es10.2)', 'difference: ', a - b
print '(a, l1)', 'abs(a - b) <= tol * abs(b): ', abs(a - b) <= tol * abs(b)
! maxval of an empty array.
print '(a, es12.4)', 'maxval of an empty array: ', maxval([real(dp) ::])
print '(a, i0)', 'maxval of an empty integer array: ', maxval([integer ::])
end program loops_and_reals
i after do i = 1, 5 : 6
trips of do i = 5, 1: 0, i = 5
n after loop = 10, i after loop = 4
0.1 + 0.2 == 0.3: F
difference: 5.55E-17
abs(a - b) <= tol * abs(b): T
maxval of an empty array: -1.7977+308
maxval of an empty integer array: -2147483648
Both compilers print the same, and GNU Fortran adds the warnings Warning: DO loop at (1) will be executed zero times [-Wzerotrip]
and the -Wcompare-reals warning already shown. What the program demonstrates:
-
The do variable after the loop. After
do i = 1, 5the variable is 6, not 5: the loop increments it before it tests the condition. After adowith zero trips the variable is the initial value, 5 here (11.1.7.4.1 and 11.1.7.4.3). Code that usesiafter the loop, for instance as the result of a search, mustexitout of the loop or save the index. -
Loop bounds are evaluated once. The statement
n = 10in the body does not change the three iterations, because the iteration count is established before the loop starts (11.1.7.4.1). To get a loop whose condition is re-evaluated, usedo whileordowithexit. -
Real numbers are compared with a tolerance.
0.1_dp + 0.2_dp == 0.3_dpis false (the difference is 5.55E-17, one unit in the last place), and the right test is a relative tolerance,abs(a - b) <= tol * abs(b), withtola small multiple ofepsilon(b). See Floating-Point and IEEE Arithmetic. -
Reductions over empty arrays return identities.
maxvalof a zero-size real array is-huge(x)(the output-1.7977+308is-huge(1.0_dp)written withes12.4because the exponent needs three digits), andmaxvalof an empty integer array is-2147483648: the standard says the negative number of the largest magnitude supported by the processor (16.9.138).sumgives 0 andproductgives 1. A program that takes themaxvalof the residual of an empty subdomain gets a huge negative number, not an error.
Diagnostic flags that catch these traps
The flags used above, in one place. The first column names the compiler, because the two compilers support different sets:
Flang 23 takes individual warnings with -W<warning> and has -fstack-arrays, but -Wextra is not supported and -Wall is
ignored (flang-23 -Wall printed argument unused during compilation), and it has no equivalent of -fcheck=, -finit-real
or -ffpe-trap (see
Compilers and Compiler Options).
| Compiler | Flag | What it reports |
|---|---|---|
gfortran |
|
the baseline used by every example: |
gfortran |
|
also conversions that cannot change a value, such as |
gfortran |
|
compile-time notice for each array temporary created for an argument |
gfortran |
|
reallocation of a left side by assignment |
gfortran |
|
the literal aliasing of an |
gfortran |
|
unused dummy arguments; calls without an explicit interface |
gfortran |
|
run-time checks, among them |
gfortran |
|
initialize uninitialized locals to a recognizable value |
gfortran |
|
stop at the first floating-point exception, with a backtrace under |
gfortran |
|
where arrays live; stack usage warnings; trapping integer overflow |
Flang 23 |
|
a default real literal in a wider context |
Flang 23 |
|
array temporaries on the stack |
All the flags above were taken from gfortran --help=warnings, --help=fortran and --help=common, and from flang-23 --help
and --help-hidden, in the container on 2026-10-19. A plain fpm build passes none of the warning flags, so a project that
relies on it sees none of the warnings above until the flags are added to the profile; see
Fortran Package Manager (fpm).
Where the books are outdated
The two Fortran books consulted for this section mostly list these traps correctly, because the rules have not changed since Fortran 90. What has moved:
| Area | Outdated in the books, current approach |
|---|---|
Allocatable and |
the books teach |
|
unit numbers are chosen by the programmer in the older text; |
Real literals |
|
Run-time checking |
no mention of the compiler-specific checks; |
Diagnostics |
the code is compiled with the compiler’s defaults; with the flags of the previous section most of the traps above are reported, and |
Stack and heap |
automatic arrays are described as ordinary local variables; on modern systems a large one is a stack overflow whose threshold depends on the compiler, optimization level and operating system limit |
Compiler context |
no compiler-specific behaviour; on this page GNU Fortran 16.2 and LLVM Flang 23 differ on the aliasing result, the unallocated |
Standard version provenance and compiler support
| Feature | Introduced | Compiler support checked on 2026-10-19 |
|---|---|---|
implicit |
long-standing rules of the language (not dated here); current text in J3/23-007r1 8.4, 8.5.10, 13.10.4 and 10.1.5.5.1 |
all compilers |
allocatable dummies deallocated by |
Fortran 2003 (allocatable dummy arguments, listed under TR 15581 in the GNU Fortran Fortran 2003 status page) |
both; the unallocated case behaves differently, see above |
|
Fortran 2008 |
both |
|
Fortran 2003 |
both |
Fortran 2023 additions that touch these traps |
none: the rules of this page are unchanged in Fortran 2023 |
not applicable |
See History, Standards and Compilers for the
per-compiler support table, and
Legacy Features and Modernization for the traps that come from
fixed form, common and equivalence, which this page leaves out.
Pitfalls
-
Trusting a program because it printed a plausible answer. Out-of-bounds reads, uninitialized variables and aliased arguments all produced a plausible result in at least one compiler above. Build with
-fcheck=alland-std=f2023in development. -
Testing with one compiler. Flang accepted the unallocated
intent(out)array and the out-of-bounds read that gfortran stopped on; gfortran accepted the 400 MB automatic array that Flang crashed on. -
Fixing a trap by changing a default.
-fdefault-real-8,-finit-local-zeroandulimit -s unlimitedhide the cause and change the program for other compilers. -
Using list-directed output for anything that is compared or parsed.
-
Hard-coding unit numbers in library code.
-
Warnings ignored in a build that scrolls past. Treat the warnings of the flags above as errors with
-Werrorin continuous integration, after cleaning them up once.
See also Best Practices and Style and Performance and Memory Layout.