Pointers and targets
Overview
Teaching: 10 min
Exercises: 10 minQuestions
How do I use a pointer to reference another variable?
Can I alias variable names?
Can I change the sizes of allocatable arrays?
Objectives
Understand how to create pointers, targets, and how to associate the two to create references.
Learn how to use an
associate
block to provide aliases to variables.Learn how to efficiently reallocate storage for an allocatable array if its size needs to be changed.
Pointer attribute
A pointer may be declared by adding the pointer
attribute to the relevant data
type, e.g.,
integer, pointer :: p => null()
In this case we declare a pointer to integer p
, which is initialised to the
special value null()
. The pointer is said to be unassociated. This is a
different state to undefined (i.e., without initialisation).
Note that pointer assignment uses =>
and not =
. A common cause of errors is
to forget the >
if pointer assignment is intended.
It is important to be able to check that a given pointer is not null()
. This
is done with the associated()
intrinsic; schematically,
integer, pointer :: p => null()
...
if (associated(p)) ... do something
If a pointer is not initialised as pointing to a target variable, it should be
initialised with null()
to avoid undefined behaviour when testing with
associated()
. Pointers may otherwise be used in expressions and assignments in
the usual way.
If one wishes to have a pointer to an array, the rank of the pointer should be the same as the target:
real, dimension(:), pointer :: p1
real, dimension(:,:), pointer :: p2
and so on.
Targets
A pointer may be associated with another variable of the appropriate type (which is not itself a pointer) by using the target attribute:
integer, target :: datum
integer, pointer :: p
p => datum
The pointer is now said to be associated with the target. We can now perform
operations on datum
vicariously through p
. E.g., a standard assignment would
be
integer, target :: datum = 1
integer, pointer :: p
p => datum ! pointer assignment
p = 2 ! normal assignment
leaves us with datum = 2
. There is no dereferencing in the C fashion; the data
is moved as the result of the normal assignment.
The target attribute is there to provide information to the compiler about which
variables may be, and which variables may not be, associated with a pointer.
This somewhat in the same spirit as the C restrict
qualifier.
Note that there is an optional target argument to the associated()
intrinsic, which allows the programmer to inquire whether a pointer is
associated with a specific target, e.g.,
associated(p, target = datum) ! .true. if p => datum
Exercise (2 minutes)
Associating a pointer
Try to compile the accompanying example1.f90, which is an erroneous version of the code above. See what compiler message you get (if any). Fix the problem.
Check you can use the
associated()
function to print out the status ofp
before and after the pointer assignment.Solution
The
p
pointer can’t be associated withdatum
as the latter doesn’t have thetarget
attribute; the compilation should fail with an error. If you add the attribute, it should compile and run:integer, target :: datum = 1
Check the
associated()
status ofp
along these lines:print *, "p associated?", associated(p)
Pointers as aliases
One common use of pointers is to provide a temporary alias to another variable
(where no copying takes place). As a convenience, one can use the associate
construct, e.g.:
real :: improbably_or_tediously_long_variable_name
...
associate(p => improbably_or_tediously_long_variable_name)
! ... lots of operations involving p ...
end associate
Note that there is no requirement here to have the target
attribute in the
original declaration (and there’s no explicit declaration of p
). Any update to
p
in the associate block will be reflected in the target on exit.
Multiple associations can be made in the same block; simply provide a comma separated list in the parentheses. Also of note is that the selector on the right hand side of the association can be either a variable or an expression.
Exercise (2 minutes)
Using an
associate
constructCompile, and check the output of the accompanying code in example2.f90.
Solution
You should see that the association is made to a strided section of the array
r1
:associate(p => r1(2::2))
Within the
associate
blockp
acts to point to the second, fourth and sixth elements ofr1
.
Pointers to establish storage
One common use of pointers is for linked data structures. For example, an entry in a linked list might be represented by the type
type :: my_node
integer :: datum
type (my_node), pointer :: next
end type my_node
This sort of dynamic data structure requires that we establish or destroy storage as entries are added to the list, or removed from the list.
subroutine my_list_add_node(head, datum)
! Insert new datum at head of list
type (my_node), pointer, intent(inout) :: head
integer, intent(in) :: datum
type (my_node), pointer :: pnode
allocate(pnode) ! assume no error
pnode%datum = datum
pnode%next => head
head => pnode
end subroutine my_list_add_node
In the subroutine, we can see the pointer pnode
is allocated. This dynamically
creates a my_node
variable to contain the new datum. At the end of the
procedure, the pnode
pointer is itself used as a target for the head
pointer.
Pointer or allocatable array?
The question may now arise: should you use pointers or allocatable arrays? If
you just want to establish storage for arrays, the answer is almost certainly
that you should use allocatable
. An allocatable array will almost certainly be
held contiguously in memory, whereas pointers are a more general data structure
which may have to accommodate a stride.
If an allocatable array is not appropriate, then a pointer may be required.
If you just require a temporary alias, the associate
construct is recommended.
Reallocating array storage
If one needs to increase (or decrease) the size of an existing allocatable
array, the move_alloc()
intrinsic is useful. E.g., if we have an integer rank
one array
integer, dimension(:), allocatable :: iorig
and establish storage of a given size, and some relevant initialisations, we may then wish to increase the size of it.
integer :: nold
integer, dimension(:), allocatable :: itmp
nold = size(iorig)
allocate(itmp(2*nold)) ! double size; assume no error
itmp(1:nold) = iorig(1:nold) ! copy existing contents explicitly
call move_alloc(itmp, iorig)
! itmp deallocated, iorig now refers to the memory that had been itmp
This minimises the number of copies involved in re-assigning the original storage.
move_alloc()
efficiencyA thought exercise. How many copies would be required if
move_alloc()
was not available when enlarging the size of an existing allocatable array?Solution
If
move_alloc()
were not available, we would need to make two copies rather than one. Analogously to the above example, we would have to perform the following to double the storage iniorig
:integer :: nold integer, dimension(:), allocatable :: itmp nold = size(iorig) allocate(itmp(nold)) ! allocate temporary storage for original data itmp(:) = iorig(:) ! first copy from original storage into the temporary deallocate(iorig) ! deallocate the original array allocate(iorig(2*ndold)) ! and then reallocate at twice the size iorig(1:nold) = itmp(:) ! second copy from the temporary into the new storage deallocate(itmp) ! deallocate the temporary
Arrays of pointers
A small trick is required to arrange an array of pointers. Recall that
real, dimension(:), pointer :: a
is a pointer to a rank one array, and not a rank one array of pointers. If one did want an array of such objects, it can be achieved by wrapping it in a type:
type :: pointer_rr1
real, dimension(:), pointer :: p => null()
end type pointer_rr1
type (pointer_rr1), dimension(10) :: a
a(1)%p => null()
So a
is a rank one array of the new type, each element having
the component p
which is a pointer to a real rank one type.
Key Points
Pointers are extremely useful for certain types of operations: they provide a way to refer indirectly to other variables or names (they act as an
_alias_
), or can be used for establishing dynamic data structures.Remember to always initialise a pointer, to
null()
if necessary.If a pointer is allocated, memory for its type is allocated and the pointer becomes associated.
The
move_alloc()
intrinsic function moves a memory allocation from one allocatable variable to another.