ZetCode

Fortran array arguments

last modified November 14, 2026

A procedure that works on an array does not have to know its size when it is written. Fortran offers four ways to declare an array dummy argument, each with a different amount of shape information: explicit shape, where the extent is an expression passed by the caller; assumed shape, where a colon lets the compiler pass the bounds along with the data; assumed size, the old form that passes only the address; and assumed rank, which accepts an array of any rank. This article shows each form, how sections reach the procedure, and how elemental and pure procedures turn a scalar function into an array function.

Explicit-shape arguments

An explicit-shape dummy is declared with an extent, as in integer, intent(in) :: a(n), where n is another dummy argument. The procedure receives the address of the first element and trusts the caller that n elements follow. The caller may pass a larger array, in which case only the first n elements are visible, and the procedure may modify the elements when the intent allows it.

g1.f90
program g1
    implicit none
    integer :: a(5)

    a = [3, 1, 4, 1, 5]
    call show(a, 5)
    call scale(a, 5, 10)
    call show(a, 5)
contains
    subroutine show(a, n)
        integer, intent(in) :: n
        integer, intent(in) :: a(n)

        print *, a
    end subroutine show

    subroutine scale(a, n, factor)
        integer, intent(in) :: n, factor
        integer, intent(inout) :: a(n)

        a = a * factor
    end subroutine scale
end program g1

show prints the five elements, and scale multiplies each of them by ten in place, because its argument is intent(inout). The second call to show therefore prints 30, 10, 40, 10 and 50. The length is passed separately and must agree with the declaration of the actual argument; nothing checks that at run time unless bounds checking is enabled, because the procedure has no descriptor that could carry the size.

$ gfortran g1.f90 -o g1
$ ./g1
           3           1           4           1           5
          30          10          40          10          50

Assumed-shape arguments

An assumed-shape dummy is declared with a colon, as in integer, intent(in) :: a(:). The compiler passes a descriptor that contains the bounds, so the procedure can call size, lbound, ubound and shape on the argument, and it works for an array of any length. The price is that the call needs an explicit interface, which a module or an internal procedure supplies. Inside the procedure the lower bound is 1 by default, even when the actual argument was a section that started elsewhere.

g2.f90
module arraymod
    implicit none
contains
    subroutine show_bounds(a)
        integer, intent(in) :: a(:)

        print *, 'lbound:', lbound(a), 'size:', size(a)
    end subroutine show_bounds

    function total(a) result(s)
        integer, intent(in) :: a(:)
        integer :: s

        s = sum(a)
    end function total
end module arraymod

program g2
    use arraymod
    implicit none
    integer :: a(6)

    a = [1, 2, 3, 4, 5, 6]
    call show_bounds(a)
    call show_bounds(a(2:4))
    print *, 'total:          ', total(a)
    print *, 'total of a(2:4):', total(a(2:4))
end program g2

The first call sees six elements starting at index 1, and the second sees three elements, also starting at index 1, because the dummy was declared with the default lower bound. The sums are 21 for the whole array and 9 for the section 2, 3, 4. If the procedure needed the original indices it could declare a(2:), but for a sum the renumbering makes no difference. Both procedures are in a module, which is what gives the calls their explicit interfaces.

$ gfortran g2.f90 -o g2
$ ./g2
 lbound:           1 size:           6
 lbound:           1 size:           3
 total:                    21
 total of a(2:4):           9

Assumed-size arguments

The oldest form declares the last dimension with an asterisk, as in integer, intent(in) :: a(*). It passes only the address of the first element, so the procedure knows neither the size nor the bounds and must receive the length separately, exactly like an explicit-shape argument but without even the declared extent. Only the last dimension may be an asterisk, and the form survives mainly in old libraries and in code that interfaces with C.

g3.f90
program g3
    implicit none
    integer :: a(5)

    a = [1, 2, 3, 4, 5]
    call show_first(a, 3)
contains
    subroutine show_first(a, n)
        integer, intent(in) :: n
        integer, intent(in) :: a(*)
        integer :: i

        do i = 1, n
            print *, a(i)
        end do
    end subroutine show_first
end program g3

The subroutine prints the first three elements and ignores the rest, which is legal because the caller passed an array with at least three. The declaration says nothing about how many elements exist, so an index beyond the real end is undetected by the procedure itself. This is why new code prefers assumed shape whenever an explicit interface is available.

$ gfortran g3.f90 -o g3
$ ./g3
           1
           2
           3

An assumed-size array has no shape to report, and asking for its size is a compile-time error rather than a wrong answer at run time:

g3b.f90
program g3b
    implicit none
    integer :: a(5)

    a = [1, 2, 3, 4, 5]
    call show_first(a, 3)
contains
    subroutine show_first(a, n)
        integer, intent(in) :: n
        integer, intent(in) :: a(*)

        print *, size(a)
    end subroutine show_first
end program g3b

The compiler rejects size(a) because the upper bound of the last dimension is unknown. The message points at the reference and names the reason, and the fix is to pass the length as an argument, as the previous example does, or to switch the dummy to assumed shape.

$ gfortran g3b.f90 -o g3b
g3b.f90:12:22:

   12 |         print *, size(a)
      |                      1
Error: The upper bound in the last dimension must appear in the reference to the assumed size array ‘a’ at (1)

Assumed-rank arguments

Fortran 2018 adds the assumed-rank dummy, written a(..), which accepts an actual argument of any rank, including a scalar. The procedure cannot index the argument directly; it first asks for the rank with a select rank construct and handles each case in its own branch. This is how a generic wrapper can forward a scalar, a vector and a matrix to the right implementation without writing three procedures.

g4.f90
program g4
    implicit none
    integer :: v(4)
    integer :: m(2, 2)

    v = [1, 2, 3, 4]
    m = reshape([1, 2, 3, 4], [2, 2])
    call describe(v)
    call describe(m)
    call describe(7)
contains
    subroutine describe(a)
        integer, intent(in) :: a(..)

        select rank (a)
        rank (0)
            print *, 'scalar:', a
        rank (1)
            print *, 'vector of', size(a), ':', a
        rank (2)
            print *, 'matrix of', size(a), ':', a
        rank default
            print *, 'other rank'
        end select
    end subroutine describe
end program g4

The first call takes a vector of four elements, the second a 2 by 2 matrix with the same four elements, and the third a scalar. Each branch of select rank sees a dummy of the matching rank, so indexing and inquiry functions work as usual inside it. The rank default branch would catch a rank that the procedure does not handle; here it never runs.

$ gfortran g4.f90 -o g4
$ ./g4
 vector of           4 :           1           2           3           4
 matrix of           4 :           1           2           3           4
 scalar:           7

Sections and contiguity

A section passed to an assumed-shape dummy does not have to occupy consecutive memory. a(1:8:2) selects every second element, and is_contiguous reports whether an array or section is stored without gaps. A dummy declared with the contiguous attribute promises the procedure that its argument is contiguous, so when the actual argument is strided the compiler makes a contiguous temporary copy and passes that instead. The copy costs time and memory, which is why the attribute is worth adding only where the procedure relies on it.

g5.f90
program g5
    implicit none
    integer :: a(8)
    integer :: i

    a = [(i, i = 1, 8)]
    call plain(a(1:8:2))
    call needs_contiguous(a(1:8:2))
contains
    subroutine plain(x)
        integer, intent(in) :: x(:)

        print *, 'plain:  contiguous =', is_contiguous(x)
    end subroutine plain

    subroutine needs_contiguous(x)
        integer, intent(in), contiguous :: x(:)

        print *, 'contiguous dummy:     ', is_contiguous(x)
    end subroutine needs_contiguous
end program g5

Both calls pass the same strided section. The first dummy has no contiguous attribute, so the section keeps its stride and is_contiguous is false. The second dummy requires contiguous storage, so the compiler inserts a temporary, and inside the procedure the inquiry is true. The example is compiled with -fcheck=bounds rather than -fcheck=all, because the full check also prints a runtime warning each time it creates such a temporary.

$ gfortran -std=f2018 -fcheck=bounds g5.f90 -o g5
$ ./g5
 plain:  contiguous = F
 contiguous dummy:      T

Optional arrays

An array dummy can be optional, like any other argument, and the procedure tests present before using it. This allows one procedure to serve both a default case and a caller that supplies extra data. The argument must not be referenced when it is absent, so the test has to come first, and an assumed-shape optional dummy still needs an explicit interface.

g6.f90
program g6
    implicit none
    integer :: a(4)

    a = [1, 2, 3, 4]
    call report(a)
    call report()
contains
    subroutine report(a)
        integer, intent(in), optional :: a(:)

        if (present(a)) then
            print *, 'sum:', sum(a)
        else
            print *, 'no array given'
        end if
    end subroutine report
end program g6

The first call supplies the array, so the subroutine prints its sum, 10. The second call omits it entirely, and the subroutine takes the other branch. Testing present(a) is the only safe way to ask; indexing an absent argument or using it in an expression would be an error.

$ gfortran g6.f90 -o g6
$ ./g6
 sum:          10
 no array given

Elemental and pure procedures

An elemental procedure is written for scalar arguments and can be called with arrays, in which case it is applied element by element and returns an array of the same shape. Elemental procedures are pure by default, and a pure procedure promises not to modify its arguments or any global state, which lets the compiler call it from contexts that require that guarantee. Both attributes are checked when the procedure is compiled.

g7.f90
module mathmod
    implicit none
contains
    elemental function square(x) result(y)
        integer, intent(in) :: x
        integer :: y

        y = x * x
    end function square

    pure function mean(a) result(m)
        real, intent(in) :: a(:)
        real :: m

        m = sum(a) / size(a)
    end function mean
end module mathmod

program g7
    use mathmod
    implicit none
    real :: x(4)

    x = [1.0, 2.0, 3.0, 4.0]
    print *, 'squares:', square([1, 2, 3, 4])
    print *, 'mean:   ', mean(x)
end program g7

square is declared for one integer, yet the call with a four-element constructor returns 1, 4, 9 and 16, because the elemental attribute makes the compiler generate the loop. mean is an ordinary pure function over an assumed-shape array, and it returns 2.5. A pure function may not assign to its arguments, use intent(out), or write to a module variable, which is exactly the restriction that makes it safe to use inside array expressions.

$ gfortran g7.f90 -o g7
$ ./g7
 squares:           1           4           9          16
 mean:      2.50000000    

A complete example

The module below collects four procedures that work on a real array: mean and stddev are pure functions, normalize modifies the array in place into zero mean and unit standard deviation, and max_position returns the index of the largest element. The program prints the statistics, normalizes the data, and prints the result. Note that maxloc(a, 1) returns a scalar for a rank-one array, which is what the integer result needs.

g8.f90
module stats
    implicit none

contains
    pure function mean(a) result(m)
        real, intent(in) :: a(:)
        real :: m

        m = sum(a) / size(a)
    end function mean

    pure function stddev(a) result(s)
        real, intent(in) :: a(:)
        real :: s, m

        m = mean(a)
        s = sqrt(sum((a - m) ** 2) / size(a))
    end function stddev

    subroutine normalize(a)
        real, intent(inout) :: a(:)
        real :: m, s

        m = mean(a)
        s = stddev(a)
        a = (a - m) / s
    end subroutine normalize

    pure function max_position(a) result(p)
        real, intent(in) :: a(:)
        integer :: p

        p = maxloc(a, 1)
    end function max_position
end module stats

program g8
    use stats
    implicit none
    real :: x(5)

    x = [2.0, 4.0, 4.0, 4.0, 5.0]
    print *, 'mean:  ', mean(x)
    print *, 'stddev:', stddev(x)
    print *, 'max at:', max_position(x)
    call normalize(x)
    print *, 'normalized:'
    print *, x
end program g8

The five values have a mean of 3.8 and a standard deviation of about 0.98. The largest value is 5, at position 5. Normalizing subtracts the mean and divides by the standard deviation, so the result has values around zero and one, with the same shape as the input. The module procedures call each other by name, and because they are all in the same module each call has an explicit interface.

$ gfortran g8.f90 -o g8
$ ./g8
 mean:     3.79999995    
 stddev:  0.979795873    
 max at:           5
 normalized:
  -1.83711731      0.204124197      0.204124197      0.204124197       1.22474492    

Next steps

The Fortran modules article explains why the procedures above need a module to be checked, and the allocatable arrays article shows how the shape of an argument can change from call to call. The array intrinsics article covers the reductions that these procedures use, and the arrays article describes sections in more detail, and the Fortran strings article moves from arrays to text. A later article on recursion returns to procedures that call themselves.

Source

Fortran 2018 standard (J3/18-007r1)

GNU Fortran: IS_CONTIGUOUS

Fortran-lang: learning resources

In this article we have passed arrays to procedures with explicit shape, assumed shape, assumed size and assumed rank, seen how sections are re-indexed and when they are copied, made an array argument optional, and written elemental and pure procedures. The previous article, Fortran allocatable arrays, explains how those arguments get their size.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Fortran tutorials.