Memory · Lesson 15.4

Pointer arithmetic

Source
Move a pointer with + and -, which count whole elements rather than bytes, and walk an array up to an end pointer.
You'll need: Pointer, Raw memory, Array, While

In Raw memory, a pointer from Alloc was indexed like an array: values[i]. That works because a pointer can do arithmetic. Adding a number to it gives a new pointer further along in memory — and the step is measured in whole elements, not in bytes.

A step is one element

The program takes the address of the first element of three arrays and measures how far + 1 moves each pointer. Converting an address to uint shows it as a plain number of bytes, so two addresses can be subtracted:

let b = @bytes[0];
let c = @counts[0];
let x = @pixels[0];
PrintLine("+1 on *uint8 moves {} byte", ((b + 1) as uint) - (b as uint));
PrintLine("+1 on *int64 moves {} bytes", ((c + 1) as uint) - (c as uint));
PrintLine("+1 on *Pixel moves {} bytes", ((x + 1) as uint) - (x as uint));
PointerOne element is+ 1 moves
*uint8one byte1 byte
*int64eight bytes8 bytes
*Pixelthree int32s together12 bytes

The compiler multiplies by the element size for you. You always count in elements, whatever their size.

Offset and index are one thing

Indexing a pointer is just arithmetic followed by a dereference: p[i] means exactly *(p + i).

PrintLine("*(c + 2) is {}, c[2] is {}", *(c + 2), c[2]);
PrintLine("(x + 1).blue is {}", (x + 1).blue);

A field is reached through the moved pointer with a plain ., as in Pointer.

Walking to an end pointer

A pointer can also walk. The loop below keeps a second pointer, end, one past the last element:

var cursor = c;
let end = c + 4;
Print("doubled:");
while cursor < end {
    *cursor = *cursor * 2;
    Print(" {}", *cursor);
    cursor += 1;
}
flowchart LR
    c0["c<br/>10"] --- c1["c + 1<br/>20"] --- c2["c + 2<br/>30"] --- c3["c + 3<br/>40"] --- e["end = c + 4<br/>never read"]

Pointers compare by address, so cursor < end means "not there yet". end itself is never read through: it marks the place just past the array. Because cursor points into counts, the doubled values are written into the array itself, which the last line confirms.

Nothing is checked

The arithmetic is never checked. A pointer moved past the end of its storage is still a pointer, and reaching through it reads or writes memory that belongs to something else — another variable, or nothing at all. Keep an end in view, as the loop does, and never step past it.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// Adding a number to a pointer moves it by whole elements, not by bytes. If `p` points at an
// `int64`, then `p + 1` points at the next `int64`, eight bytes further on. The compiler
// multiplies by the element size for you, so the same `+ 1` steps one byte through `uint8`s
// and twelve bytes through a twelve-byte struct.
//
// That is also what indexing a pointer means: `p[i]` is exactly `*(p + i)`.
//
// The arithmetic itself is never checked. A pointer moved past the end of its storage is still
// a pointer, and reaching through it reads or writes memory that belongs to something else.
// Keep an end in view: here, a second pointer one past the last element.
import Io::{ Print, PrintLine };

struct Pixel {
    red: int32;
    green: int32;
    blue: int32;
}

func Main() -> int {
    var bytes: uint8[4] = [1, 2, 3, 4];
    var counts: int64[4] = [10, 20, 30, 40];
    var pixels: Pixel[2] = [Pixel { red: 255, green: 0, blue: 0 },
                            Pixel { red: 0, green: 0, blue: 255 }];

    // Converting an address to `uint` shows it as a number of bytes, so the step `+ 1` takes
    // can be measured. It is the element size each time.
    let b = @bytes[0];
    let c = @counts[0];
    let x = @pixels[0];
    PrintLine("+1 on *uint8 moves {} byte", ((b + 1) as uint) - (b as uint));
    PrintLine("+1 on *int64 moves {} bytes", ((c + 1) as uint) - (c as uint));
    PrintLine("+1 on *Pixel moves {} bytes", ((x + 1) as uint) - (x as uint));

    // Offset and index are two spellings of one thing.
    PrintLine("*(c + 2) is {}, c[2] is {}", *(c + 2), c[2]);
    PrintLine("(x + 1).blue is {}", (x + 1).blue);

    // Walking with a pointer: `end` is one past the last element and is never read through.
    // Pointers compare by address, so `cursor < end` means "not there yet".
    var cursor = c;
    let end = c + 4;
    Print("doubled:");
    while cursor < end {
        *cursor = *cursor * 2;
        Print(" {}", *cursor);
        cursor += 1;
    }
    PrintLine();

    // The writes went through to the array itself.
    PrintLine("counts[3] is now {}", counts[3]);
    return 0;
}

Run it

cd Examples/Memory/PointerArithmetic
rux run
+1 on *uint8 moves 1 byte
+1 on *int64 moves 8 bytes
+1 on *Pixel moves 12 bytes
*(c + 2) is 30, c[2] is 30
(x + 1).blue is 255
doubled: 20 40 60 80
counts[3] is now 80

Common mistakes

Adding bytes instead of elements.
To skip one int64 you add 1, not 8. c + 8 compiles and moves 64 bytes — far past the end of a four-element array.
Forgetting the parentheses.
*c + 2 reads the first element and adds 2 to it, giving 12. To reach two elements on, write *(c + 2), or simply c[2].
Reading the end.
while cursor <= end runs once too often and reads one element past the array. Nothing reports it. end marks the stopping place; compare with <.
Subtracting two pointers.
end - c is not allowed: it fails with error: operator '-' cannot combine left operand '*var int64' with right operand '*var int64'. To count the elements between them, convert both to uint, subtract, and divide by sizeof(int64).

Try it yourself

  1. Print the counts in reverse: start a cursor at end, and in a while cursor > c loop step it back with cursor -= 1 before each read.
  2. Count the elements between c and end with ((end as uint) - (c as uint)) / sizeof(int64).
  3. Walk pixels with a cursor and print each pixel's red and blue.

Learn more