DCL03-F. Avoid undefined behavior due to uninitialized variables
Accessing an uninitialized variable can lead to undefined behavior due to its indeterminate value.
Always initialize variables before using them, helping ensure deterministic behavior and prevent bugs in the code.
Some programming languages automatically initialize variables to default values, but Fortran, C, and C++ often do not. Consequently, variables left uninitialized contain unpredictable data until they are explicitly set by the programmer. In Fortran, reading any variable that has not been explicitly initialized results in undefined behavior.
Since uninitialized variables may contain any arbitrary values, reading and using them can lead to undefined behavior, potentially causing incorrect results, crashes, or other unintended outcomes. Compilers are not required to warn about these issues and, even if they do, they typically still allow the code to compile and run.
Some compilers may appear to "help" by zero-initializing variables under certain conditions (e.g., specific compilation flags). While this can make technically incorrect code run as originally intended, relying on such incidental behavior creates a false sense of security and masks underlying logical errors. Ultimately, this can vanish under different compilation settings and can vary between compilers.
Lastly, while some compilers provide options to automatically initialize
certain data types (e.g., gfortran's -finit-integer=<value> or gcc's
-ftrivial-auto-var-init=<value>), these features reduce portability to other
development environments and hide problems in the code rather than addressing
them.
Noncompliant Code Example
Consider the following code, which aims to sum the elements of an array:
! example_array.f90
program main
use iso_fortran_env, only: real32
implicit none
real(kind=real32) :: array(5)
array = [0.24, 0.33, 0.17, 0.89, 0.05]
print *, "Sum is:", sum_array(array)
contains
pure real(kind=real32) function sum_array(array)
implicit none
real(kind=real32), intent(in) :: array(:)
real(kind=real32) :: sum
integer :: i
do i = 1, size(array, 1)
sum = sum + array(i)
end do
sum_array = sum
end function sum_array
end program main
Note how sum is never explicitly initialized. Although it might seem like it
"should" logically start at 0, the Fortran standard does not guarantee this.
Thus, the initial value of sum is indeterminate, leading to different
outcomes depending on the compiler settings.
Implementation Details (Unix)
Consider the following compiler settings that produce different results. For instance, gfortran -O2 appears to start sum at 0, allowing the
program to work as intended:
$ gfortran --version
GNU Fortran (Debian 14.2.0-8) 14.2.0
$ gfortran -O2 example_array.f90 -o example_array_gfortran
$ ./example_array_gfortran
Sum is: 1.67999995
However, with flang -O2, sum appears to contain arbitrary data, leading
to incorrect results:
$ flang-new --version
Debian flang-new version 19.1.5 (1)
$ flang-new -O2 example_array.f90 -o example_array_flang
$ ./example_array_flang
Sum is: NaN
Compliant Solution
The solution is straightforward, always initialize variables before using them:
real(kind=real32) :: sum
sum = 0
This principle applies to all variable types, including other elemental types
like integer, derived types, and arrays.
Noncompliant Code Example
Arrays are a critical part of simulation codes. For managing dynamic,
n-dimensional arrays in Fortran, both pointer and allocatable variables are
available. The latter, introduced in Fortran 2003, are generally safer and more
robust. Unlike pointer, variables with the allocatable attribute
automatically free their memory and are always set by default to the
unallocated state.
For example, the following code technically leads to undefined behavior because
a pointer is used without explicit initialization:
program main
implicit none
integer, pointer :: array(:)
if (.not. associated(array)) then
print *, "Undefined behavior"
end if
end program main
Compliant Solution
In contrast, an allocatable array can always be safely checked using the
allocated function, even when not explicitly initialized:
program main
implicit none
integer, allocatable :: array(:)
if (.not. allocated(array)) then
print *, "Defined behavior"
end if
end program main
While these examples may seem trivial, these types of issues can arise in large, complex codebases where variables traverse multiple procedures and are subject to intricate conditional logic.
If, for any reason, you still need to use pointer variables, it's a good
practice to nullify them as early as possible for additional safety. For module
variables and derived type initializations, you can nullify right at the point
of declaration:
type :: t
integer, pointer :: array(:) => null()
end type
But within procedures, it's best to declare the pointer variable first and then
nullify it. This avoids inadvertently introducing an implicit save behavior:
integer, pointer :: array(:)
nullify(array)
Risk Assessment
Undefined behavior can produce incorrect results, silent data corruption, crashes, or nondeterministic behavior that varies across compilers or platforms. Programmers should ensure that the code avoids undefined behavior in all cases.
| Recommendation | Severity | Likelihood | Detectable | Repairable | Priority | Level |
| DCL03-F | High | Likely | Yes | Yes | P27 | L1 |
Attachments:
button_arrow_left.png (image/png)
button_arrow_up.png (image/png)
button_arrow_right.png (image/png)


