Trick for optional arguments with defaults

It’s very common when dealing with optional arguments to have a local variable that is assigned either a default value or the value of a present argument, like:

subroutine bar(...,a)
    real, intent(in), optional :: a
    real :: a_local
    a_local = 2.0; if (present(a)) a_local = a
    ... ! use only a_local in the further code
end subroutine

And it’s also very common to use the argument a by mistake instead of a_local, resulting in a segmentation violation during execution if a was not present.

A way to prevent such an error is to use an ASSOCIATE block that spans the whole routine:

subroutine bar(...,a)
    real, intent(in), optional :: a
    real :: a_local
    a_local = 2.0; if (present(a)) a_local = a
    ASSOCIATE( a => a_local )
    ... ! "a" can now be safely used as an alias to a_local
    END ASSOCIATE
end subroutine

The dummy argument a is shadowed by the alias a inside the block. Another benefit is that we don’t have to use the local variable name with a somehow artificial suffix (_local or whatever)

7 Likes

I only just started using blockend block in things like loops to declare local variables and such in a smaller scope.

Can you nest these structures? Meaning can you have multiple associate or block constructs within each other?

In theory, you can have as many nested associate and block constructs as you want.

In practice:

The associate-construct is a thin pseudo-scope (the underlying behavior is that of pointers and shadowing), so you could nest as many as you want… until it starts getting confusing.

The block-construct, on the other hand, is a true scope, so it’s bound by the stack’s limits (and recursion complicates things).

Both constructs are also bound by the compiler’s rules (e.g., for ifort, I think the magic nesting number was 31, all constructs combined).

We also created the optval function in stdlib exactly for this purpose.

1 Like

The two approaches are actually complementary. optval() is fine as long as the argument is used once or twice. If repeatedly used it will tend to be less readable than a local variable (and a change of the default value requires several changes in the code).

They can also be combined (not tested, but should work):

ASSOCIATE( a => optval( a, 2.0 ) )
1 Like

ASSOCIATE blocks work, more or less, like invoking a contained procedure with actual arguments being associated with the contained procedure dummy arguments. However, one difference is that ASSOCIATE blocks are allowed to be nested arbitrarily by the standard (at least to some compiler limit), while CONTAINS is limited to only one level (or two, if you count the CONTAINS statement in a module). A programmer runs into that CONTAINS limit when converting statement functions in legacy codes to contained procedures within a module. That limit has always annoyed me, I guess for being so arbitrary and seemingly unnecessary. You can usually work around the problem by flattening the hierarchy, but it sometimes requires renaming the functions to avoid conflicts.

My Pure-Fortran project has a script to warn about the unsafe use of optional arguments. For the code

module m
implicit none
contains
function max2(x1, x2) result(xmax)
real, intent(in) :: x1
real, intent(in), optional :: x2
real :: xmax
if (present(x2)) then
   xmax = max(x1, x2)
else
   xmax = x1
end if
end function max2

function max_(x1, x2, x3, x4, x5) result(xmax)
real, intent(in) :: x1, x2
real, optional, intent(in) :: x3, x4, x5
real :: xmax
xmax = max(x1, x2)
if (present(x3)) xmax = max(x3, xmax)
xmax = max2(xmax, x4) ! allowed since 2nd argument of max2 is OPTIONAL
xmax = max(xmax, x5)  ! unsafe since x5 may not be PRESENT
end function max_
end module m

use m
print*,max_(3.2, 2.6)
print*,max_(3.2, 2.6, 4.5)
end

python xoptional.py xoptional.f90 gives

1 optional-argument guard finding(s) in 1 file(s) (definite 1, possible 0).
xoptional.f90: 1

First finding: xoptional.f90:22 function max_ x5 [definite] - optional argument may be used without guaranteed PRESENT() guard