34 min read

> *"I call it my billion-dollar mistake: the invention of the null reference in 1965. … It has led to

Prerequisites

  • 5
  • 6
  • 8
  • 9

Learning Objectives

  • Distinguish pointer assignment (=>) from value assignment (=), and predict which object each one modifies.
  • Declare pointer and target entities and query a pointer's association status with associated, disassociating it with nullify or => null().
  • Identify and avoid the two pointer hazards — undefined status and dangling pointers — and free the nodes of a linked structure without leaking memory.
  • Build a linked list with pointers, and explain why a contiguous array usually outperforms a linked structure in Fortran.
  • Choose between allocatable and pointer with a clear rule, and justify the default preference for allocatable by automatic deallocation and the no-aliasing optimization.
  • Apply the contiguous attribute (and the is_contiguous intrinsic) to an array pointer or dummy argument, and state exactly what promise it makes to the compiler.

Chapter 11: Pointers, Targets, and Dynamic Data Structures

"I call it my billion-dollar mistake: the invention of the null reference in 1965. … It has led to innumerable errors, vulnerabilities, and system crashes, probably a billion dollars of pain and damage in the last forty years." — C. A. R. (Tony) Hoare, reflecting on null references, QCon London 2009

Overview

Every chapter so far has handed you a tool and told you to reach for it. This one hands you a tool and spends most of its length telling you not to. That is not a contradiction — it is the single most important thing to understand about pointers in Fortran. Fortran has pointers, they are powerful, and in day-to-day numerical code you should reach for them rarely, because the language gives you something better for almost everything you were tempted to use them for: the allocatable arrays and components you already met in Chapter 5 and Chapter 9.

A pointer is a variable that does not hold a value of its own; instead it is an alias — a second name for some other object, or for a block of memory you allocated at run time. If you come from C, you already have a mental picture, and it is mostly wrong for Fortran: a Fortran pointer is dereferenced automatically (you never write *p), it carries the shape of an array it points at, and it can only point at things that gave it explicit permission. If you come from Python, the picture is different again: in Python essentially every variable is a reference, so b = a for a list makes two names for one object. Fortran is the opposite — assignment copies values by default, and aliasing happens only when you ask for it, with a distinct operator. That distinction, => versus =, is where this chapter begins, and getting it exactly right is what separates code that works from code that silently corrupts.

We will build the real thing — a linked list, node by node, with pointers — because you must know how, and because you will inherit code that does it. But we will then make the case, honestly and with the performance reasoning this book is built on, that in Fortran a plain array usually wins: it is faster, safer, and simpler. By the end you will have a rule you can apply without thinking — prefer allocatable; reach for pointer only when you genuinely need aliasing, polymorphic containers, callbacks, or C interoperability — and you will understand the one performance attribute, contiguous, that pointers bring to the table when you do need them.

In this chapter, you will learn to:

  • Declare pointer and target entities, and tell pointer assignment (p => a, aliasing) apart from value assignment (p = a, copying through the alias).
  • Track a pointer's association status, test it with associated, and clear it with nullify or => null() — and recognize the two ways a pointer goes bad: undefined and dangling.
  • Assemble and tear down a dynamic structure (a linked list) without leaking a single node.
  • Decide between allocatable and pointer from a short, principled rule — and explain why the compiler makes fast code from the former.
  • Use the contiguous attribute to promise the compiler a strided pointer is really a solid block of memory, and verify that promise with is_contiguous.

Learning Paths

How to read this chapter by track. - 🔬 Scientist ("my code just needs to run fast and be right") — read §11.1–11.2 for the mechanics, then §11.4 closely; it is the rule that will keep your numerical code both fast and leak-free. You can skim the linked-list mechanics in §11.3 and take its conclusion on faith. - 📖 Standard — read straight through; pointers are a corner of the language you should understand fully even though you will use them sparingly. - 🔧 Legacy ("I inherited old code") — §11.2 and §11.3 are your survival kit: pointer-based structures and their bugs (leaks, dangling aliases) are exactly what you will find and have to fix. - ⚡ HPC ("I need parallel code") — §11.4 (no aliasing → optimization) and §11.5 (contiguous) are your sections; they connect directly to Chapter 27 and Chapter 29.


11.1 pointer, target, and the Two Kinds of Assignment

Start with the mental model, because everything else is a consequence of it. A normal variable is a box that holds a value. A pointer is not a box; it is a label on a string that you can tie to some other box — or to nothing at all. Tie it to a box, and from then on the pointer's name means "the box I am tied to." Untie it and retie it elsewhere, and the same name now means a different box. The pointer never holds the value itself; it only ever refers.

Definition (pointer). A pointer is a variable declared with the pointer attribute that holds no value of its own but can be associated with — made an alias for — another object, or with a block of memory obtained by allocate. Wherever you use the pointer's name in an expression, Fortran automatically uses the object it is associated with; there is no explicit dereference operator like C's *p. A pointer to an array carries that array's shape and bounds, so you can slice and index it exactly as if it were the array.

There is a matching permission on the other side. A pointer cannot alias just any variable — only one that has volunteered to be aliased, by carrying the target attribute.

Definition (target). The target attribute marks an object as something a pointer is allowed to point at. Aiming a pointer at a variable that lacks both target and pointer is not allowed. The rule exists for the compiler's benefit: an ordinary variable, not marked target, is guaranteed to have no pointer aliasing it, so the compiler may keep it in a register and optimize around it freely. target is you telling the compiler, "give up that assumption for this object — a pointer may be watching it."

Here is the smallest program that shows a pointer being tied to a target and used:

program pointer_intro
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  real(dp), target  :: a = 1.0_dp     ! a volunteers to be pointed at
  real(dp), pointer :: p => null()     ! p starts tied to nothing

  p => a                               ! POINTER assignment: p is now an alias for a
  print '(a, f0.1)', 'a via p = ', p   ! reading p reads a -> 1.0
end program pointer_intro
$ gfortran -std=f2018 -Wall pointer_intro.f90 -o pintro && ./pintro
a via p = 1.0

Notice three things. The target a was declared target; the pointer p was declared pointer and initialized => null() (more on why in §11.2); and the association was made with =>, not =. That operator choice is the crux of the whole chapter.

=> aliases; = copies

Fortran gives you two assignment operators near a pointer, and they do completely different things.

Definition (pointer assignment). Pointer assignment, written with the arrow p => a, associates the pointer p with the target a — it makes p an alias for a, changing what p refers to but touching no numerical value. Ordinary value assignment, written p = a, does not change what p points to; if p is already associated, it copies the value of a through p, into the target p currently refers to. One arrow changes the alias; one equals sign changes the data.

Read that twice, then watch it happen. This program ties p to a, writes through the alias, re-ties p to b, and writes again — and we track exactly which real value changes at each step:

program alias_vs_copy
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  real(dp), target  :: a = 1.0_dp, b = 2.0_dp
  real(dp), pointer :: p => null()

  p => a                 ! p aliases a          (=>  changes the alias)
  p = 5.0_dp             ! writes 5 THROUGH p into a   (=  changes the value)
  print '(a, f0.1, a, f0.1)', 'after p=>a, p=5:   a = ', a, ',  b = ', b

  p => b                 ! now p aliases b instead; a is left completely alone
  p = 9.0_dp             ! writes 9 into b
  print '(a, f0.1, a, f0.1)', 'after p=>b, p=9:   a = ', a, ',  b = ', b
end program alias_vs_copy
$ gfortran -std=f2018 -Wall alias_vs_copy.f90 -o alias && ./alias
after p=>a, p=5:   a = 5.0,  b = 2.0
after p=>b, p=9:   a = 5.0,  b = 9.0

Trace it against the two definitions. p => a aliases a; p = 5.0_dp writes 5 through the alias, so a becomes 5 while b is untouched at 2. Then p => b re-aims the alias at b — crucially, this does not copy a into b; it only changes what p means — so a stays 5. Finally p = 9.0_dp writes 9 into b. End state: a = 5.0, b = 9.0. If you had written p = b instead of p => b at the third step, you would have copied b's value into a (through the still-a-aliased p), a completely different result. The operator is not a stylistic choice; it selects the operation.

💡 Intuition: think of => as moving a sticky note and = as writing on the page the sticky note is stuck to. Moving the note (p => b) changes nothing on any page; it just relabels which page p names. Writing through the note (p = 9) changes the page under it. Almost every pointer bug in existence is someone who moved the note when they meant to write, or wrote when they meant to move.

🐍 Python Comparison: Python has the opposite default. In Python, b = a for a mutable object (a list, a NumPy array) makes b a second reference to the same object — aliasing is what you get unless you deliberately copy. In Fortran, b = a copies, and aliasing is what you get only when you deliberately ask, with =>. This is why Fortran numerical code is easier to reason about — and easier for the compiler to optimize: it does not have to assume every assignment might have created a hidden alias. The Python default is convenient for scripting; the Fortran default is a gift to the optimizer.

📜 From History: Fortran had allocatable arrays and pointers arrive close together, both in the Fortran 90 modernization, but for different jobs: allocatable for "I need an array whose size I learn at run time," and pointer for "I need aliasing and linked structures." Over the following standards the committee kept expanding what allocatable could do — allocatable components (2003), allocatable dummy arguments and function results, automatic (re)allocation on assignment — precisely so that pointers could retreat to the narrow set of jobs only they can do. The language's own evolution is an argument for the rule this chapter teaches.


11.2 Association Status: associated, nullify, and Two Ways to Go Wrong

A pointer is always in exactly one of three states, and knowing which is the difference between a correct program and one with a landmine in it.

Definition (association status). A pointer's association status is one of: associated — it is currently a valid alias for some target or allocated memory; disassociated — it is explicitly tied to nothing, the state produced by nullify(p) or by initializing p => null(); or undefined — it has never been given a status at all, the state of a freshly declared pointer with no initialization. The intrinsic associated(p) returns .true. for the first and .false. for the second — but you must never call it on an undefined pointer, because there is nothing valid for it to inspect.

The associated intrinsic comes in two forms. associated(p) asks "is p tied to anything?" associated(p, tgt) asks the sharper question "is p tied to this specific target tgt?" Both are constantly useful — the first to guard against using a null pointer, the second to check identity. And nullify(p) is how you deliberately untie a pointer, setting it to the clean, testable disassociated state.

program association_status
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  real(dp), target  :: a = 1.0_dp, b = 2.0_dp
  real(dp), pointer :: p => null()          ! DISASSOCIATED from the start — good habit

  print '(a, l1)', 'associated(p) at start      = ', associated(p)      ! F
  p => a
  print '(a, l1)', 'associated(p) after p=>a    = ', associated(p)      ! T
  print '(a, l1)', 'associated(p, a) same target= ', associated(p, a)   ! T
  print '(a, l1)', 'associated(p, b) same target= ', associated(p, b)   ! F
  nullify(p)
  print '(a, l1)', 'associated(p) after nullify = ', associated(p)      ! F
end program association_status
$ gfortran -std=f2018 -Wall association_status.f90 -o assoc && ./assoc
associated(p) at start      = F
associated(p) after p=>a    = T
associated(p, a) same target= T
associated(p, b) same target= F
associated(p) after nullify = F

Every line is checkable against the definition: p begins disassociated (we initialized it that way), so associated(p) is F; after p => a it is T; it is tied to a and not to b, so the two-argument form reports T then F; and after nullify(p) it is disassociated again, back to F.

Hazard one: the undefined pointer

Look again at the declaration real(dp), pointer :: p => null(). That => null() is not decoration. A pointer declared without it —

real(dp), pointer :: p        ! no initializer: p has UNDEFINED status

— has undefined association status. It is neither associated nor disassociated; it holds garbage. And here is the trap: associated(p) on an undefined pointer is itself undefined behavior. It may return .true., it may return .false., it may crash — and worst of all, it may do the right thing on your machine today and the wrong thing on the cluster tomorrow. You cannot test your way out of an undefined pointer, because the test is part of the undefined behavior.

⚠️ Common Pitfall — undefined and dangling pointers. These are the two ways a Fortran pointer betrays you, and both are silent.

Undefined: a pointer you never initialized. The fix is a one-time habit — always give a pointer a status the moment it is born, by initializing => null() in its declaration or calling nullify before first use. Then associated(p) is always a meaningful question.

Dangling: a pointer that was valid but whose target no longer exists — you deallocated the memory through another pointer, or the target went out of scope. The pointer still looks associated; associated(p) may cheerfully return .true.; but it now aliases freed or vanished memory, and touching it is undefined behavior. This one is worse than undefined, because it passes every test you can write. The fix is discipline: after you deallocate through one pointer, nullify every other pointer that aliased the same memory — the language will not do it for you.

The dangling case deserves to be seen, because it is the bug that eats afternoons. This fragment is wrong — do not model code on it:

real(dp), pointer :: p => null(), q => null()
allocate(p)                ! p now owns a fresh real
q => p                     ! q aliases the SAME allocated memory
deallocate(p)              ! frees the memory; the standard disassociates p...
                           ! ...but q is untouched, and now DANGLES.
print *, associated(q)     ! may print T — a lie; q points at freed memory
print *, q                 ! undefined behavior: reading freed memory

We show it as a fragment, and we do not paste an "expected output," precisely because there is no defined output — that is the whole point. After deallocate(p), the standard sets p to disassociated, but q is a separate pointer variable that the deallocation never heard of. q still carries the old address; it dangles. The honest fix is to treat the deallocation as the death of every alias: deallocate(p) then nullify(q), and never touch q's data again.

🔗 Connection: This is exactly why the allocatable arrays of Chapter 5 cannot dangle: an allocatable is the sole owner of its memory, there is no second alias to be left behind, and when it dies the memory dies with it, automatically. Hold that thought — it is half of the argument in §11.4. When allocation errors themselves must be handled (a grid too large for memory), the stat=/errmsg= machinery you will meet in Chapter 13 applies to both pointers and allocatables.

🔄 Check Your Understanding. 1. What are the three possible association statuses of a pointer, and which one must you never pass to associated? 2. Why is initializing a pointer => null() in its declaration a good default? 3. After deallocate(p) where q => p had aliased the same memory, what is wrong with q, and what must you do?

Answers 1. Associated, disassociated, and undefined. Never call associated on an undefined pointer — the result is itself undefined behavior. 2. It gives the pointer a defined (disassociated) status from birth, so associated(p) is always a meaningful test and you can never accidentally use an undefined pointer. 3. q dangles — it aliases freed memory while possibly still reporting associated(q) == .true.. You must nullify(q) and never dereference it again; the language will not clean up aliases for you.


11.3 Dynamic Structures: A Linked List, a Tree, and Why Arrays Usually Win

Here is the classic reason people reach for pointers: dynamic data structures whose shape is not known until run time and changes as the program runs — a list that grows, a tree that branches. The enabling trick is that a derived type may contain a pointer to its own type, which lets you chain nodes together.

A linked list

A singly linked list is a chain of nodes, each holding a value and a pointer to the next node; the last node's pointer is disassociated, marking the end.

 head --> [ 3 | *-]--> [ 2 | *-]--> [ 1 | / ]
           val next      val next      val next=null()

The node type and the operations are short. We build the list by prepending (cheap: no traversal), which means values come out in reverse — push 1, 2, 3 and the list reads 3, 2, 1, the last-in-first-out order of a stack:

program linked_list
  implicit none

  type :: node_t
    integer :: val
    type(node_t), pointer :: next => null()   ! a pointer to the SAME type
  end type node_t

  type(node_t), pointer :: head => null()
  integer :: k

  do k = 1, 3                 ! prepend 1, 2, 3  ->  list becomes 3 -> 2 -> 1
    call prepend(head, k)
  end do

  call show(head)
  call destroy(head)          ! free every node; leaves head disassociated
  print '(a, l1)', 'associated(head) after destroy = ', associated(head)

contains

  subroutine prepend(list, value)
    type(node_t), pointer, intent(inout) :: list
    integer,               intent(in)    :: value
    type(node_t), pointer :: fresh
    allocate(fresh)             ! one node's worth of memory, at run time
    fresh%val  = value
    fresh%next => list          ! new node points at the old head
    list       => fresh         ! head becomes the new node
  end subroutine prepend

  subroutine show(list)
    type(node_t), pointer, intent(in) :: list
    type(node_t), pointer :: p
    integer :: length, total
    length = 0
    total  = 0
    write(*, '(a)', advance='no') 'values head->tail:'
    p => list
    do while (associated(p))    ! walk until the end-of-list null
      write(*, '(a, i0)', advance='no') ' ', p%val
      length = length + 1
      total  = total + p%val
      p => p%next               ! step to the next node
    end do
    write(*, '(a)') ''          ! end the line
    print '(a, i0, a, i0)', 'length = ', length, ',  sum = ', total
  end subroutine show

  subroutine destroy(list)
    type(node_t), pointer, intent(inout) :: list
    type(node_t), pointer :: p, nxt
    p => list
    do while (associated(p))
      nxt => p%next             ! save the next pointer BEFORE freeing p...
      deallocate(p)             ! ...or we would be reading freed memory
      p => nxt
    end do
    nullify(list)               ! the head is now invalid; make it honest
  end subroutine destroy

end program linked_list
$ gfortran -std=f2018 -Wall linked_list.f90 -o list && ./list
values head->tail: 3 2 1
length = 3,  sum = 6

Read the three routines against the picture. prepend allocates one fresh node, points its next at the current head, and makes the head the new node — that is the whole insert, and it is $O(1)$. show walks the chain with a traversal pointer p, following p => p%next until associated(p) goes false at the terminating null, accumulating the length (3) and the sum ($3+2+1=6$) as it goes. destroy is the one to study: it saves p%next into nxt before calling deallocate(p), because after the node is freed its next field is gone — reading it would be the dangling-pointer bug from §11.2. Free first, read never; save, free, step.

🐛 Find the Bug. A colleague simplifies destroy to save a line:

fortran p => list do while (associated(p)) deallocate(p) ! <-- frees the node... p => p%next ! <-- ...then reads p%next from the freed node end do

What goes wrong, and what is the fix?

Diagnosis After deallocate(p), the node's memory — including its next field — is gone, so p => p%next reads freed memory: undefined behavior (a crash if you are lucky, silent corruption if you are not). The order must be save the next pointer, then free: introduce nxt => p%next before deallocate(p), then p => nxt. You cannot read a node after you have freed it.

A tree

The same idea, with two child pointers instead of one next, gives a binary tree — the backbone of sorted sets, expression trees, and spatial partitions:

type :: tree_t
  real(dp)              :: key
  type(tree_t), pointer :: left  => null()
  type(tree_t), pointer :: right => null()
end type tree_t

Insertion walks down from the root, going left for smaller keys and right for larger, allocating a new node at the empty branch it reaches; traversal recurses into left, visits the node, recurses into right. The mechanics are a direct extension of the list, so we will not belabor a full program — the important lesson is not how to build a tree in Fortran but whether you should, and the answer, more often than newcomers expect, is no.

Why arrays usually win

Everything you have just seen works. It is also, for most numerical work in Fortran, the slow way to solve the problem — and understanding why is worth more than the syntax.

A linked structure scatters its nodes across memory: each allocate hands you a node from wherever the allocator happens to have room, so consecutive nodes are, in general, nowhere near each other. Walking the list therefore means pointer chasing — load a node, read its next address, jump to a random new location, stall while the processor fetches a cache line it cannot predict, repeat. This defeats the two things modern hardware is fastest at: prefetching (the CPU cannot guess where p%next leads, so it cannot fetch ahead) and vectorization (you cannot apply one SIMD instruction across nodes that are not adjacent). On top of that, every node costs a separate allocation, and allocation is not free.

Contrast a plain array. Its elements are contiguous, so a sweep walks memory in a straight line the prefetcher loves; the compiler can vectorize the loop; and there is exactly one allocation for the whole thing. This is the array superpower from Chapter 5, seen from the other side: the very contiguity that makes arrays fast is what a linked list throws away.

🚪 Threshold Concept — in Fortran, reach for an array first. In a language built for pointer-linked structures, a linked list is the natural default and an array is a special case. Fortran is the reverse. Its whole performance story is contiguous memory swept in order, and a pointer-linked structure opts out of that story on every node. So invert the instinct you may have brought from C or Java: do not ask "which linked structure fits this problem?" Ask "can this be an array?" — and it can, far more often than you would think. A stack or a growable list is an allocatable array you resize (you will build exactly that in this chapter's second case study). A sparse graph is often an index-based structure: store each node's "next" as an integer index into an array, not a pointer, and you keep the dynamic shape while regaining contiguity, prefetching, and the ability to save the whole thing to a file as one block. The linked list is a tool of last resort in Fortran, not first.

⚡ Performance Note: The gap is not subtle. Summing ten million integers stored in a contiguous array versus the same ten million in a linked list is, on typical hardware, an order-of-magnitude difference in favor of the array — not because the arithmetic differs (it is identical) but because the array sweep hits cache and vectorizes while the list sweep stalls on unpredictable memory (a Tier 2 order-of-magnitude, not a promise; the direction, though, is never in doubt). When a genuinely dynamic, frequently-restructured graph really does call for links, the index-based array representation usually still beats raw pointers, and it is what large scientific codes overwhelmingly use.

🔄 Check Your Understanding. 1. In destroy, why must nxt => p%next come before deallocate(p)? 2. Name two hardware advantages a contiguous array has over a linked list when you sweep through it. 3. What is an "index-based" alternative to a pointer-linked list, and what does it recover?

Answers 1. Because deallocate(p) frees the node, including its next field; reading p%next afterwards reads freed memory (a dangling access). Save the next pointer while the node is still alive. 2. Prefetching (contiguous access is predictable, so the CPU fetches ahead) and vectorization (one SIMD instruction can process adjacent elements). A scattered list allows neither. 3. Store the nodes in an array and represent "next" as an integer index into that array instead of a pointer. It recovers contiguity, prefetching, vectorizability, and one-block file I/O, while keeping a dynamic, linked logical shape.


11.4 allocatable vs pointer: The Rule

You now have both dynamic-memory tools in hand — the allocatable arrays and components from Chapters 5 and 9, and the pointer of this chapter — and they overlap: both can obtain memory at run time with allocate. So which do you use? For numerical Fortran the answer is a rule you can apply almost without thinking, and it is worth stating in bold before we justify it: prefer allocatable; use pointer only when you actually need what only a pointer can do. Here is the side-by-side that earns the rule.

allocatable pointer
Ownership Sole owner of its memory May alias other pointers/targets
Aliasing None possible (unless also target) Yes — that is its purpose
Deallocation Automatic on scope exit Manual — you must deallocate, or leak
Dangling Impossible Possible (freed/out-of-scope target)
Undefined status No — allocated() is always valid Yes — undefined until initialized
Assignment b = a Deep copy, auto-(re)allocated to fit Copies the value through, if associated
Compiler optimization Full no-aliasing freedom Conservative (must assume aliasing)
Can point at existing data No Yes
Linked structures Awkward Natural

Three of those rows are the whole argument, and each is a theme of this book.

Automatic deallocation (safety). A local allocatable array or component is freed automatically when its scope ends — you cannot leak it by forgetting, and you cannot make it dangle because nothing else aliases it. A pointer, like C's malloc, is yours to free by hand on every exit path, and the error branch you forgot is the leak you ship. This is the "modern Fortran is a modern language" theme made concrete: the language manages the memory so you do not have to.

No aliasing (speed). This is the deep one. Because a plain allocatable array can never be aliased by a pointer, the compiler knows that writing to it cannot change any other array — so it may keep values in registers, reorder loads and stores, and vectorize freely. A pointer, by contrast, might alias another pointer, so when the compiler sees writes through one and reads through another it must assume the worst and generate cautious, slower code. This is the no-aliasing advantage that Chapter 27 is devoted to — the same property, first met in Chapter 1, that makes Fortran outrun C on numerical kernels. Reaching for a pointer where an allocatable would do can forfeit that advantage, quietly, for no benefit. Performance is not accidental; here it is a direct consequence of which attribute you typed.

⚡ Performance Note: Even giving an array the target attribute — not making it a pointer, just permitting pointers to alias it — can cost you optimization, because it tells the compiler "a pointer may be watching this," switching off some of the no-aliasing freedom. So the counsel is stronger than "prefer allocatable": keep your hot arrays plain allocatable, with neither pointer nor target, unless you have a concrete reason to relax that. Your heat solver's field, as the Project Checkpoint shows, stays exactly that plain.

Assignment does the right thing. For an allocatable b, the statement b = a deep-copies a, allocating or reallocating b to the correct shape automatically (the F2003 behavior gfortran gives you by default at -std=f2018). You get a genuine independent copy with one equals sign. For pointers, b = a copies values through whatever b and a already point at — a different operation with different preconditions, and a common source of surprise.

🔗 Connection — allocatable components beat pointer components. You met this already in Chapter 9: a derived type that needs a run-time-sized array inside it uses an allocatable component, not a pointer component, for exactly the reasons above — the component is deep-copied when the whole object is assigned, and freed automatically when the object dies. The heat solver's field_t, with its real(dp), allocatable :: u(:,:), is the canonical example, and this chapter is why it is written that way.

So when is a pointer the right tool? When you need one of the things only aliasing can give — and that is the entire subject of §11.5.

🔄 Check Your Understanding. 1. Give the one-line rule for choosing between allocatable and pointer. 2. Why can the compiler optimize a plain allocatable array more aggressively than a pointer? 3. What does b = a do when both are allocatable arrays of different sizes?

Answers 1. Prefer allocatable; use pointer only when you genuinely need aliasing, a linked structure, a polymorphic container, a callback, or C interoperability. 2. A plain allocatable cannot be aliased by any pointer, so the compiler knows a write to it cannot change any other array — the no-aliasing advantage — and may reorder and vectorize freely. A pointer might alias, forcing conservative code. 3. It deep-copies a into b, automatically reallocating b to a's shape first — an independent copy, correctly sized, from one assignment.


11.5 When You Do Need Pointers — and the contiguous Attribute

The rule is "prefer allocatable," not "never use pointers." There are jobs only a pointer can do, and a professional reaches for one without guilt when the job is one of these:

  • Aliasing an existing object — giving a compact name to a piece of a larger structure, or letting two parts of a program share one object. An allocatable owns memory and cannot point at data that already exists; a pointer can.
  • Genuine linked structures — when the problem really is a frequently-restructured graph or list and an index-based array will not serve (§11.3), pointers are the honest tool.
  • Polymorphic containers — a list or array meant to hold objects of different dynamic types uses class(...)-pointer or allocatable components; this is the OOP machinery of Chapter 10, where select type sorts out what you actually have at run time.
  • Callbacks — a procedure pointer lets you store which function to call in a variable, so one generic routine can be handed different behaviors. It is how you pass a right-hand-side function to a generic ODE integrator (the method-of-lines idea of Chapter 23), or a comparison function to a sort.
  • C interoperability — the C world is built on pointers, so talking to it (§Chapter 14) means handling C addresses as type(c_ptr) and turning them into usable Fortran pointers with c_f_pointer.

Procedure pointers: storing "which function to call"

A procedure pointer points at a procedure instead of at data. You declare it against an interface, aim it at any procedure matching that interface, and call through it — and you can re-aim it at run time.

program callback_demo
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none

  abstract interface
    function unary(x) result(y)        ! the shape every target must match
      import :: dp
      real(dp), intent(in) :: x
      real(dp)             :: y
    end function unary
  end interface

  procedure(unary), pointer :: f => null()

  f => square
  print '(a, f0.1)', 'f => square;  f(3.0) = ', f(3.0_dp)   ! 9.0
  f => negate
  print '(a, f0.1)', 'f => negate;  f(3.0) = ', f(3.0_dp)   ! -3.0

contains
  function square(x) result(y)
    real(dp), intent(in) :: x
    real(dp)             :: y
    y = x * x
  end function square
  function negate(x) result(y)
    real(dp), intent(in) :: x
    real(dp)             :: y
    y = -x
  end function negate
end program callback_demo
$ gfortran -std=f2018 -Wall callback_demo.f90 -o cb && ./cb
f => square;  f(3.0) = 9.0
f => negate;  f(3.0) = -3.0

The pointer f is aimed at square, called (giving $3^2 = 9$), then re-aimed at negate and called again (giving $-3$) — one call site, two behaviors, chosen at run time. That is the callback pattern, and it is genuinely a pointer's job: there is no allocatable equivalent, because what varies is not data but code to run. (Note the import :: dp inside the interface body: an interface is its own scope and does not see the host's used entities unless you import them.)

C interoperability, in one line

When Fortran receives a bare memory address from a C library — the return of a C malloc, say, arriving as a type(c_ptr) — you cannot index it directly; you convert it to a real Fortran pointer of a known shape with c_f_pointer:

real(dp), pointer :: buf(:)
call c_f_pointer(c_handle, buf, [n])   ! now buf(1:n) aliases the C array

This is illustrative — the full story, with iso_c_binding, type(c_ptr), and passing arrays and structs across the language boundary, is Chapter 14. The point for now is only that here the pointer is unavoidable: C hands you an address, and a pointer is Fortran's word for "an address I can use as an array."

The contiguous attribute

When you do use an array pointer, you inherit a performance problem the compiler cannot solve on its own. A pointer may be aimed at a strided slice of an array — every third element, or a row of a column-major matrix — whose elements are not adjacent in memory. Because the compiler must assume any array pointer might be strided, it generates general, slower code for it, even when at run time the pointer always happens to point at a solid block. The contiguous attribute is how you close that gap.

Definition (contiguous). The contiguous attribute (Fortran 2008), applied to an array pointer or an assumed-shape dummy argument, is your promise to the compiler that the array occupies a single unbroken block of memory — no strides, no gaps. It changes no results; it licenses optimization, letting the compiler treat the array as the simple contiguous span it is, which is what makes vectorization and efficient memory streaming possible. It is a promise you must keep: aim a contiguous pointer at a genuinely strided slice and the program is nonconforming (the compiler may silently copy, or misbehave). The intrinsic is_contiguous(a) reports, at run time, whether an array actually is contiguous.

In column-major storage (Chapter 5), a full column of a matrix is contiguous, while a row is strided — so a contiguous pointer may legitimately view a column but not a row. This program aims a contiguous pointer at each column of a small grid, and uses is_contiguous to confirm the column/row distinction:

program contiguous_demo
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  integer, parameter :: n = 3
  real(dp), allocatable, target :: field(:,:)
  real(dp), pointer, contiguous :: col(:)
  integer :: i, j

  allocate(field(n, n))
  do j = 1, n
    do i = 1, n
      field(i, j) = real(10*i + j, dp)   ! field(i,j) announces its own address
    end do
  end do

  ! A whole column is contiguous in column-major order; a whole row is strided.
  print '(a, l1)', 'is_contiguous(field(:,2))  [column] = ', is_contiguous(field(:,2))
  print '(a, l1)', 'is_contiguous(field(2,:))  [row]    = ', is_contiguous(field(2,:))

  do j = 1, n
    col => field(:, j)         ! legal: a column IS contiguous, so the promise holds
    print '(a, i0, a, f0.1)', 'sum of column ', j, ' = ', sum(col)
  end do
  nullify(col)

  deallocate(field)
end program contiguous_demo
$ gfortran -std=f2018 -Wall contiguous_demo.f90 -o contig && ./contig
is_contiguous(field(:,2))  [column] = T
is_contiguous(field(2,:))  [row]    = F
sum of column 1 = 63.0
sum of column 2 = 66.0
sum of column 3 = 69.0

With field(i,j) = 10i + j, column 1 holds 11, 21, 31 (sum 63), column 2 holds 12, 22, 32 (sum 66), and column 3 holds 13, 23, 33 (sum 69) — each a hand-checkable total. And is_contiguous confirms the rule that governs when the contiguous promise is safe to make: a column comes back T, a row F.

🔗 Connection: contiguous is a first taste of a performance tool you will use in anger in Chapter 29, where the more common application is on an assumed-shape dummy argument — real(dp), intent(inout), contiguous :: u(:,:) — to promise a kernel that its caller always passes solid storage, so the compiler can vectorize the sweep. There it is one of the levers, alongside loop ordering and cache blocking, that turns correct code into fast code. You are meeting the attribute here, where pointers live; you will spend it there.

🔄 Check Your Understanding. 1. Name three situations where a pointer is genuinely the right tool, not an allocatable. 2. What promise does contiguous make, and what happens if you break it? 3. For a column-major matrix a(n,n), is a(:,3) contiguous? Is a(3,:)?

Answers 1. Aliasing an existing object; a genuine (index-based won't do) linked structure; a polymorphic container; a callback via a procedure pointer; C interoperability (c_f_pointer). Any three. 2. That the array is a single unbroken block of memory (no strides/gaps), which licenses vectorization and efficient streaming. Break it — aim a contiguous pointer at a strided slice — and the program is nonconforming; the compiler may copy or misbehave. 3. a(:,3) (a full column) is contiguous; a(3,:) (a row) is strided, hence not contiguous, because the first index varies fastest in column-major storage.


Project Checkpoint

This checkpoint is a decision, revisited and now justified. Back in Chapter 9 you gave the heat solver a field_t derived type, and its grid was declared

real(dp), allocatable :: u(:,:)

— an allocatable component, not a pointer component. This chapter is where you can finally say exactly why, in one sentence per reason:

  1. Automatic deallocation. When a field_t goes out of scope, its u array is freed automatically — no manual deallocate on every exit path, no leak on the error branch you forgot. A pointer component would put that bookkeeping (and its bugs) on you.
  2. No aliasing → a faster stencil. A plain allocatable array cannot be aliased by any pointer, so when Chapter 24 sweeps the five-point Laplacian across u, the compiler knows the writes to the new field cannot secretly change the old one, and it vectorizes and reorders freely. That is the no-aliasing advantage of Chapter 27 — and a pointer field would forfeit it.

The following short program makes the choice concrete. The field is allocatable (given target here only so we can demonstrate a pointer view); a contiguous pointer sweeps its columns; and there is no deallocate at the end, because none is needed:

program checkpoint_field_is_allocatable
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  real(dp), allocatable, target :: u(:,:)      ! `target` only for the demo below
  real(dp), pointer, contiguous :: col(:)
  integer :: j

  allocate(u(3,3))
  u = reshape([real(dp) :: 1, 2, 3,  4, 5, 6,  7, 8, 9], [3, 3])   ! column-major

  do j = 1, 3
    col => u(:, j)                 ! a column is contiguous, so the promise holds
    print '(a, i0, a, f0.1)', 'column ', j, ' sum = ', sum(col)
  end do
  nullify(col)
  ! No deallocate(u): it is freed automatically when the program unit ends.
end program checkpoint_field_is_allocatable
$ gfortran -std=f2018 -Wall project-checkpoint.f90 -o checkpoint && ./checkpoint
column 1 sum = 6.0
column 2 sum = 15.0
column 3 sum = 24.0

The reshape fills u column-major, so column 1 is [1, 2, 3] (sum 6), column 2 is [4, 5, 6] (sum 15), and column 3 is [7, 8, 9] (sum 24) — verify each against the printed line. One honest caveat that is itself part of the lesson: in the real solver, u carries no target attribute, because — as the Performance Note in §11.4 warned — permitting pointer aliasing can cost optimization. We added target here purely to show contiguous in action. When Chapter 29 optimizes the sweep, it applies contiguous not to a pointer aliasing the field but to the kernel's assumed-shape dummy argument, keeping the field itself pristine. Record in your heat-solver/ notes the one-line rule you will live by for the rest of the book: the field is allocatable, never a pointer.


Summary

Pointers are a real part of Fortran and a small part of good numerical Fortran. This chapter taught the mechanics so you can read and fix any pointer code — and taught the judgment to write very little of it.

Idea The short version
pointer / target A pointer is an alias for another object; the object must be target (or itself pointer) to be aliased.
=> vs = p => a changes the alias (points p at a); p = a changes the value (copies through an associated p).
Association status associated / disassociated / undefined; associated(p) tests the first two, is illegal on the third.
nullify / => null() Set a pointer to the clean disassociated state; initialize every pointer this way at birth.
Undefined pointer Never initialized; associated on it is undefined behavior. Fix: always => null() or nullify first.
Dangling pointer Aliases freed/out-of-scope memory; may still report associated. Fix: nullify every alias after deallocate.
Linked structures A derived type with a pointer to its own type; free nodes save-then-deallocate, never read a freed node.
Arrays usually win Contiguous memory prefetches and vectorizes; pointer-chasing does neither. Prefer arrays / index-based structures.
allocatable vs pointer Prefer allocatable: automatic deallocation, no dangling, no aliasing → full optimization.
When to use pointer Aliasing, genuine linked structures, polymorphic containers, callbacks (procedure pointers), C interop.
contiguous A promise that an array pointer / dummy is one solid block; licenses vectorization. is_contiguous tests it.

The two things to memorize. First, the operator distinction: => moves the alias, = writes the value — confuse them and you corrupt data silently. Second, the rule that governs the whole chapter: prefer allocatable; a pointer is a last resort you reach for only when you need aliasing, links, polymorphism, callbacks, or C. Everything else here is detail in service of those two sentences.

Spaced Review

Retrieval practice on the two chapters this one builds on most directly — arrays (Ch. 5) and derived types (Ch. 9). Answer before peeking.

  1. (Ch. 5) Why does a local allocatable array not leak memory even if you never call deallocate, and how does that contrast with a pointer?

    Answer A local allocatable is automatically deallocated when its procedure returns — the language frees it for you on scope exit. A `pointer` has no such guarantee: you must `deallocate` it by hand on every exit path, and forgetting leaks. This is one of the pillars of §11.4's "prefer allocatable" rule.

  2. (Ch. 5) In Fortran's column-major layout, which is contiguous in memory — a row a(i,:) or a column a(:,j) — and why does this determine when a contiguous pointer is legal?

    Answer A column `a(:,j)` is contiguous, because the first index varies fastest, so a whole column sits in one unbroken run of memory; a row `a(i,:)` is strided. You may aim a `contiguous` pointer at a column but not at a row — the promise must actually hold. (Confirmed with `is_contiguous` in §11.5.)

  3. (Ch. 5) What does the array section u(2:n-1, 2:n-1) select, and is it something a pointer could alias?

    Answer It selects the interior of the grid — every element except the outermost row and column on each side — as a first-class rank-2 array value. A pointer can alias such a section (if the parent has `target`), but the section is strided in general, so a plain array pointer, not a `contiguous` one, is what fits.

  4. (Ch. 9) In a derived type that needs a run-time-sized array inside it, why is an allocatable component preferred over a pointer component?

    Answer The allocatable component is deep-copied when the whole object is assigned and freed automatically when the object is destroyed, with no aliasing and no dangling — the same three advantages as §11.4, now at the component level. A pointer component would demand manual cleanup and could alias or dangle. The heat solver's `field_t` uses `real(dp), allocatable :: u(:,:)` for exactly this reason.

  5. (Ch. 9) A field_t bundles nx, ny, dx, dy and the grid u. If you assign b = a for two field_t values, what happens to a's data — is b%u a copy or an alias of a%u?

    Answer It is an independent copy. Intrinsic assignment of a derived type deep-copies each component, and because `u` is allocatable, `b%u` is allocated to `a%u`'s shape and its values copied — two separate arrays. Had `u` been a pointer component, the default assignment would have made `b%u` alias `a%u` instead (pointer components are copied by association, not by value) — a classic aliasing surprise, and one more reason the component is allocatable.

What's Next

You can now alias when you must, and — more importantly — you know when not to. That completes the pair of dynamic-memory tools: allocatable for owning data (the default) and pointer for referring to it (the exception). Chapter 12 turns to a data type we have used only in passing — text. You will meet deferred-length (character(:), allocatable) strings, which are just the allocatable idea of this part applied to characters; the string intrinsics that trim, scan, and search; and internal files, which let you read numbers out of a string and write them back in. It is where your solver learns to build output filenames like heat_000123.vtk — and where you will be glad, one more time, that Fortran's default is to own its memory and clean up after itself.