Best practices: Shorthand to derived type components

Questions related to this one have been posted before, but I could not find a satisfying recommendation for my particular use case. I am looking for some advice about best practices in making code that is readable while also avoiding undesirable effects.

I have a subroutine that operates on a number of components of a derived type. To simplify the argument list, I just pass the complete structure to the subroutine:

subroutine K_unsat(soilstate)

use parametersmod, only : sp
use typesmod,      only : soilstatetype

implicit none

! argument

type(soilstatetype), dimension(:), target, intent(inout) :: soilstate

Because I need to update various components of the derived type, and to avoid writing cumbersome code, I experimented with giving the argument the target attribute and creating pointers as shorthand to the components:

! pointers to state variables

real(sp), dimension(:), pointer :: Bexp
real(sp), dimension(:), pointer :: ddKuTliq
real(sp), dimension(:), pointer :: ddPsiTliq
real(sp), dimension(:), pointer :: ffrz
real(sp), dimension(:), pointer :: Ksat
real(sp), dimension(:), pointer :: Ku
real(sp), dimension(:), pointer :: psi_e
real(sp), dimension(:), pointer :: psi
real(sp), dimension(:), pointer :: Sr
real(sp), dimension(:), pointer :: T_wat
real(sp), dimension(:), pointer :: Tice
real(sp), dimension(:), pointer :: Tliq
real(sp), dimension(:), pointer :: Tsat

Then I assigned these pointers to the respective components of the derived type argument:

! variable and pointer assignment

Bexp      => soilstate%Bexp
ddKuTliq  => soilstate%ddKuTliq
ddPsiTliq => soilstate%ddPsiTliq
ffrz      => soilstate%ffrz
Ksat      => soilstate%Ksat
Ku        => soilstate%Ku
psi_e     => soilstate%psi_e
psi       => soilstate%psi
Sr        => soilstate%Sr
T_wat     => soilstate%T_wat
Tice      => soilstate%Tice
Tice      => soilstate%Tliq
Tsat      => soilstate%Tsat

and then I do my calculations, using the pointers as shorthand to the components, for example (shortened for clarity):

do l = 1,nl

  ...

  psi(l) = psi_e(l) * Sr(l)**(-Bexp(l))

  ...

  ddPsiTliq(l) = -Bexp(l) * psi(l) / (Sr(l) * Tsat(l))

end do

end subroutine K_unsat

My question is, is there a better way to achieve this kind of shorthand to individual structure components? I looked in to an associate block, but that would also lead to a gnarly line of code if I was defining local names to 10+ structure components. Creating new variables in the local context would require a bunch of allocate statements and variable assignments at the beginning and end of the routine, although I suppose that might make the intent of each component more obvious.

Thanks for your advice!

Fortran doesn’t have a pascal-like with statement. So associate blocks are the closest you can get. In theory compilers can generate more efficient code with associate than by using pointers as well.

If you are trying to find way to avoid the pointers because of alias optimization problems, then one possibility is to shorten the variable name and just use the original component definitions.

S%psi(l) = S%psi_e(l) * S%Sr(l)**(-S%Bexp(l))

That requires only two characters more than the pointer approach for each component reference. You could also do this with an ASSOCIATE block.

you dont need one associate selector per component, thats the trick. a single associate (s => soilstate) gives you short names for everything, s%Bexp, s%ddKuTliq, the lot, and you can still assign to the components through it. it also dodges the aliasing problem the pointer version has, since the compiler knows s is just another name for soilstate. id drop the pointers for exactly that reason.

That’s also a hurdle for me. I remember reading somewhere here that some in compilers associate is replaced by a local pointer variable and since then I got afraid using them in performance critical kernels. When I need to shorten long names, I just use preprocessor directives:

#define s obj%nested_1%nested_2
    s%psi(l) = s%psi_e(l) * ...
#undef

I would love to see some feature like using(obj%nested) or with which operates at syntax/parsing level.

I can understand that fear. But here is the thing: a Fortran POINTER is nothing more than a description of a data item’s location in the storage system, extended to arrays (which are “regular”, in a precise sense, due to the language’s restrictions on pointer association). Furthermore, the POINTER feature was designed so that it is (easy to become) obvious to the compiler that the target of a pointer remains the same across some statements, in most of the commonly occurring cases that it does so. For instance, the compiler can “see” that a particular procedure reference cannot possibly result in changing the target of a pointer actual argument, because it has access to an explicit interface for that procedure (and that interface does not specify POINTER for that dummy argument). Now, there’s always the risk that a particular compiler implementation “forgot” to make use of this, and that is something that a compiler vendor would like to know that they got wrong.

Let’s fight that fear with understanding!

A Fortran pointer is just an “alias” for some target —the target may have been anonymously created through the allocate statement, or it can be another variable (that has the pointer or target attribute).

That “alias” usually changes where it points to only through the => assignment or some argument association/initialization.

But in regards to the associate-construct, the standard explicitly states that an association doesn’t have the POINTER or ALLOCATABLE attribute, which means that, even if the compiler’s underlying mechanism uses pointers for the association (which is kind of the obvious choice), said associations never stop pointing where stated in the associate-stmt.

So the use of the associate-stmt is actually an advantage in terms of optimization opportunities (and safe for PURE procedures), and the only drawbacks are usually just the ones for invalid code, e.g.:

integer, allocatable :: a
a = 1
associate (b => a)
    deallocate (a)
    print *, b
end associate

Since a got deallocated, b status became undefined, and it shouldn’t be used anymore —but that’s just invalid code, not the associate-construct’s fault.

One advantage of the associate-construct is that it doesn’t have the dangling pointer problem, e.g.:

implicit none
integer, allocatable :: a
a = 1
associate (b => a)
    print*,b
    deallocate (a)
    a = 2
    print*,b
end associate
end
$ ifort -diag-disable=10448  associate-stmt.f90 && ./a.out 
           1
           2

I’m not sure this is “legal”. From the standard, J3/24-007, Section 9.7.3.2:

An allocatable variable shall not be deallocated if it or any subobject of it is argument associated with a dummy argument or construct associated with an associate name.

It does happen to work with some compilers.

So the program should be,

implicit none
integer, allocatable :: a
a = 1
associate (b => a)
    print *,b
    a = 2
    print *,b
end associate
deallocate(a)
end

Hmm… since it happens to work just fine with ifort, ifx, gfortran, flang-21 and even nvfortran, I thought the code was legal. Did they all got it wrong?

:man_shrugging: Given that b is considered not to have allocatable or pointer attributes, it does seem logical that the allocation status of a should be left unchanged for the duration of the construct. But this is just my interpretation.

It’s not “wrong” to do anything outside the interpretation given by the Standard. The program has no interpretation, so anything is arguably correct. It has become a bad random number generator.

Since the quote from section 9.7.3.2 is not a constraint, compilers are not required to report it (or even regard it?).

Moreover, section 10.2.1.3 establishes that the left-hand side of an intrinsic assignment where an allocatable array is involved might be implicitly deallocated (and then allocated to match the shape of the right-hand side).

So, imho, there seems to be some conflict there where said assignment occurs within an associate-construct, but I’m no expert.

Is there a Doctor (Fortran) in the room thread? :laughing:

No, they are not required to report it. Many such cases.

Although I remember having read that it is illegal to deallocate the thing on the RHS of =>, I’ve just tried the following code for comparison:

implicit none
integer, allocatable :: a
a = 1
print *, "loc(a) = ", loc(a)
associate (b => a)
  deallocate (a)        !! (L1)
  print *, "b = ", b
  print *, "loc(b) = ", loc(b)
end associate
end

Then, gfortran-15 (on macOS) gives

 loc(a) =       105553155080240
 b =     47333424
 loc(b) =       105553155080240

and, after commenting out the L1 line, it gives:

 loc(a) =       105553120936032
 b =            1
 loc(b) =       105553120936032

The result of flang-22 is also similar (i.e., b gives a garbage value if L1 is included). Options like gfortran -fcheck=all and flang -pedantic do not say anything, so I guess it is the programmer’s responsibility to avoid such deallocation (while compilers are not required to diagnose them because potentially too much complicated).

Just as a workaround, is it possible to write some macro so that multiple sub-components are imported more conveniently via associate or #define by using variadic cpp macro or fypp variadic macro…? For example,

with( foo, a, b, ... ) 
...
endwith

may be expanded to

associate( a => foo% a, &
           b => foo% b, &
                 ... )
....
end associate

(though it is probably impossible to extract all the components automatically).

I’m still wary of using associate as popular compilers like gfortran and ifx (also nvfortran) still have some bugs in the associate statement.

Consider this program:

program assoc_overlap
  implicit none
  integer :: i
  real :: a(6)
  a = [(real(i), i = 1, 6)]
  associate (an => a(2:5), ae => a(3:6), aw => a(1:4))
    an = ae - aw   ! same as a(2:5) = a(3:6) - a(1:4)
  end associate
  print '(6f5.1)', a   ! expected: 1.0 2.0 2.0 2.0 2.0 6.0
  if (any(a /= [1., 2., 2., 2., 2., 6.])) stop 1
  print '(a)', 'PASS'
end program

gfortran 16.2, ifx 2025.3.2, LFortran 0.67 print:

 1.0  2.0  2.0  3.0  3.0  6.0
STOP 1

nvfortran 26.9 and flang (trunk) print:

  1.0  2.0  2.0  2.0  2.0  6.0
PASS

The same statement written with plain sections, or with a single associate (x => a), gives the right answer. Results shown here: Compiler Explorer

Edit: I edited my post because of some mistakes.

Edit 2: reported to LFortran: Missing temporary when ASSOCIATE names alias overlapping sections of the same array · Issue #14985 · lfortran/lfortran · GitHub

I’ve had some issues in the past when using associate with expressions, but whole variables seemed fine.

In your example, the left-hand side is being partially updated during the subtraction. I wonder if it’s some kind of optimization (i.e., if it also occurs when -g -O0 are in play).

EDIT: Nop, the behavior is the same with ifx and gfortran, regardless of optimization level.

If this were a subroutine call rather than an associate block, then this would be illegal because of the argument aliasing (and because an(:) is modified). Does associate allow this kind of aliasing?

Now I’m amusingly confused.

What the associate is doing, for the failing compilers, is

a(2) = a(3} - a(1)
a(3) = a(4) - a(2)
a(4) = a(5) - a(3)  ! here, a(3)'s value is the new one
a(5) = a(6) - a(4)  ! here, a(4)'s value is the new one

With that, assuming that the underlying associate implementation behaves like a subroutine, aliasing is avoided.

But the trick to pass the test is to just consider the right-hand side of the assignment as an expression (which is exactly what compilers usually do in absence of the associate-construct), thus:

program assoc_overlap
  implicit none
  integer :: i
  real :: a(6)
  a = [(real(i), i = 1, 6)]
  associate (an => a(2:5), ae => (a(3:6)), aw => (a(1:4)))
    an = ae - aw   ! same as a(2:5) = a(3:6) - a(1:4)
  end associate
  print '(6f5.1)', a   ! expected: 1.0 2.0 2.0 2.0 2.0 6.0
  if (any(a /= [1., 2., 2., 2., 2., 6.])) stop 1
  print '(a)', 'PASS'
end program
$ ifort -diag-disable=10448 -C -standard-semantics -stand f23 assoc-mismatch.f90 && ./a.out 
  1.0  2.0  2.0  2.0  2.0  6.0
PASS

Unfortunately gfortran gets this case wrong too, see:

  • Bug 121384 - Wrongly initialized associate array descriptor when the target is wrapped in parenthesis
  • Bug 127120 - ASSOCIATE mishandles a parenthesized variable as the selector

That said, gfortran 17 will bring a number of fixes to associate.