Skip to content

Arrays

Arrays have a central role in programming. C3 offers built-in arrays, slices and vectors. The standard library enhances this further with dynamically sized arrays and other collections.

Fixed Size 1D Arrays

These are declared as <type>[<size>], e.g. int[4]. Fixed arrays are treated as values and will be copied if given as parameter. Unlike C, the number is part of its type. Taking a pointer to a fixed array will create a pointer to a fixed array, e.g. int[4]*.

Unlike C, fixed arrays do not decay into pointers. Instead, an int[4]* may be implicitly converted into an int*.

// C
int foo(int *a) { ... }

int x[3] = { 1, 2, 3 };
foo(x);

// C3
fn int foo(int* a) { ... }

int[3] x = { 1, 2, 3 };
foo(&x);

When you want to initialize a fixed array without specifying the size, use the [*] array syntax:

int[3] a = { 1, 2, 3 };
int[*] b = { 4, 5, 6 }; // Type inferred to be int[3]

You can get the length of an array using the .len property:

int len1 = int[4].len; // 4
int[3] a = { 1, 2, 3 };
int len2 = a.len; // 3
int[*] b = { 1, 2 };
int len3 = b.len; // 2

Indexing into pointers of arrays

A source of confusion going from C to C3 is that indexing into, for example, a pointer int[3]* would yield an int[3], rather than an int. To get the integer inside of the array that is pointed to, we need to do a dereference:

int[3] a = { 1, 2, 3 };
int[3]* b = &a;
int x = (*b)[1]; // Correctly returns 2
// Broken: int x = b[1]

A convenient shorthand for (*b)[1] is to use implicit subscript dereference: b.[1]. Here the . is only doing a dereference if the variable is a pointer. So given the example above we have:

a[1];    // Returns 2
a.[1];   // Returns 2
b[1];    // BROKEN! Out of bounds access
(*b)[1]; // Returns 2
b.[1];   // Returns 2

This feature is mainly useful in generic modules and macros.

Slice

The final type is the slice <type>[] e.g. int[]. A slice is a view into either a fixed or variable array. Internally it is represented as a struct containing a pointer and a size. Both fixed and variable arrays may be converted into slices, and slices may be implicitly converted to pointers.

fn void test()
{
    int[4] arr = { 1, 2, 3, 4 };
    int[4]* ptr = &arr;

    // Assignments to slices
    int[] slice1 = &arr;                // Implicit conversion
    int[] slice2 = ptr;                 // Implicit conversion

    // Assignments from slices
    int[] slice3 = slice1;              // Assign slices from other slices
    int* int_ptr = slice1;              // Assign from slice
    int[4]* arr_ptr = (int[4]*)slice1;  // Cast from slice
}

Slicing Arrays

It's possible to use the range syntax to create slices from pointers, arrays, and other slices.

This is written arr[<start-index> .. <end-index>], where end-index is inclusive.

fn void test()
{
    int[5] a = { 1, 20, 50, 100, 200 };

    int[] b = a[0 .. 4]; // The whole array as a slice.
    int[] c = a[2 .. 3]; // { 50, 100 }
}

You can also use arr[<start-index> : <slice-length>]

fn void test()
{
    int[5] a = { 1, 20, 50, 100, 200 };

    int[] b2 = a[0 : 5]; // { 1, 20, 50, 100, 200 } start-index 0, slice-length 5
    int[] c2 = a[2 : 2]; // { 50, 100 } start-index 2, slice-length 2
}

It’s possible to omit the first and last indices of a range: - arr[..<end-index>] Omitting the start index will default it to 0 - arr[<start-index>..] Omitting the end index will assign it to arr.len-1 (this is not allowed on pointers)

Equivalently with index offset arr[:<slice-length>] you can omit the start-index

The following are all equivalent and slice the whole array

fn void test()
{
    int[5] a = { 1, 20, 50, 100, 200 };

    int[] b = a[0 .. 4];
    int[] c = a[..4];
    int[] d = a[0..];
    int[] e = a[..];

    int[] f = a[0 : 5];
    int[] g = a[:5];
}

You can also slice in reverse from the end with ^i where the index is len-i for example: - ^1 means len-1 - ^2 means len-2 - ^3 means len-3

Again, this is not allowed for pointers since the length is unknown.

fn void test()
{
    int[5] a = { 1, 20, 50, 100, 200 };

    int[] b1 = a[1 .. ^1];  // { 20, 50, 100, 200 } a[1 .. (a.len-1)]
    int[] b2 = a[1 .. ^2];  // { 20, 50, 100 }      a[1 .. (a.len-2)]
    int[] b3 = a[1 .. ^3];  // { 20, 50 }           a[1 .. (a.len-3)]

    int[] c1 = a[^1..];     // { 200 }              a[(a.len-1)..]
    int[] c2 = a[^2..];     // { 100, 200 }         a[(a.len-2)..]
    int[] c3 = a[^3..];     // { 50, 100, 200 }     a[(a.len-3)..]

    int[] d = a[^3 : 2];    // { 50, 100 }          a[(a.len-3) : 2]

    // Slicing a whole array, the inclusive index of : gives the difference
    int[] e = a[0 .. ^1];   // a[0 .. a.len-1]
    int[] f = a[0 : ^0];    // a[0 : a.len]

}

One may also assign to slices:

int[3] a = { 1, 20, 50 };
a[1..2] = 0; // a = { 1, 0, 0 }

Or copy slices to slices:

int[3] a = { 1, 20, 50 };
int[3] b = { 2, 4, 5 };
a[1..2] = b[0..1]; // a = { 1, 2, 4 }

Copying between two overlapping ranges, e.g. a[1..2] = a[0..1] is unspecified behaviour.

Conversion List

int[4] int[] int[4]* int*
int[4] copy - - -
int[] - assign assign -
int[4]* - cast assign cast
int* - assign assign assign

Note that all casts above are inherently unsafe and will only work if the type cast is indeed compatible.

For example:

int[4] a;
int[4]* b = &a;
int* c = b;

// Safe cast:
int[4]* d = (int[4]*)c;
int e = 12;
int* f = &e;

// Incorrect, but not checked
int[4]* g = (int[4]*)f;

// Also incorrect but not checked.
int[] h = f[0..2];

Internals

Internally the layout of a slice is guaranteed to be struct { <type>* ptr; usz len; }. Note that in 0.8+, the length is sz.

There is a built-in struct std::core::runtime::SliceRaw which has the exact data layout of the fat array pointers. It is defined to be

struct SliceRaw
{
    void* ptr;
    usz len;
}

Dynamically allocated slices

Standard library provides utilities for allocating multiple elements into a slice:

// uses calloc under the hood (memory is zeroed out)
int[] arr1 = mem::new_array(int, 10);
defer mem::free(arr1);

// uses malloc under the hood (memory is undefined)
int[] arr2 = mem::alloc_array(int, 10);
defer mem::free(arr2);

Iteration Over Arrays

foreach element by copy

You may iterate over slices, arrays and vectors using foreach (Type x : array). Using compile-time type inference this can be abbreviated to foreach (x : array) for example:

fn void test()
{
    int[4] arr = { 1, 2, 3, 5 };
    foreach (item : arr)
    {
        io::printfn("item: %s", item);
    }

    // Or equivalently, writing the type:
    foreach (int x : arr)
    {
        /* ... */
    }
}

foreach element by reference

Using & it is possible to get an element by reference rather than by copy. Providing two variables to foreach, the first is assumed to be the index and the second the value:

fn void test()
{
    int[4] arr = { };
    foreach (idx, &item : arr)
    {
        *item = 7 + (int)idx; // Mutates the array element
        // index is usz when not specified, requiring an explicit
        // cast on platforms where usz is larger than int.
        // 0.8+, "sz" rather than usz is used.
    }

    // Or equivalently, writing the types
    foreach (int idx, int* &item : arr)
    {
        *item = 7 + idx; // Mutates the array element
    }
}

foreach_r reverse iterating

With foreach_r arrays or slices can be iterated over in reverse order

fn void test()
{
    float[4] arr = { 1.0, 2.0 };
    foreach_r (idx, item : arr)
    {
        // Prints 2.0, 1.0
         io::printfn("item: %s", item);
    }

    // Or equivalently, writing the types
     foreach_r (int idx, float item : arr)
    {
        // Prints 2.0, 1.0
         io::printfn("item: %s", item);
    }
}

Iteration Over Array-Like types

It is possible to enable foreach on any custom type by implementing .len and [] methods and annotating them using the @operator attribute:

struct DynamicArray
{
    usz count;
    usz capacity;
    int* elements;
}

macro int DynamicArray.get(DynamicArray* arr, usz element) @operator([])
{
    return arr.elements[element];
}

macro usz DynamicArray.count(DynamicArray* arr) @operator(len)
{
    return arr.count;
}

fn void DynamicArray.push(DynamicArray* arr, int value)
{
    arr.ensure_capacity(arr.count + 1);  // Function not shown in example.
    arr.elements[arr.count++] = value;
}

fn void test()
{
    DynamicArray v;
    v.push(3);
    v.push(7);

    // Will print 3 and 7
    foreach (int i : v)
    {
        io::printfn("%d", i);
    }
}

For more information, see operator overloading

Dynamic Arrays and Lists

The standard library offers dynamic arrays and other collections in the std::collections module.

alias ListStr = List {String};

fn void test()
{
    ListStr list_str;

    // Initialize the list on the heap.
    list_str.init(mem);

    list_str.push("Hello");  // Add the string "Hello"
    list_str.push("World");

    foreach (str : list_str)
    {
        io::printn(str);   // Prints "Hello", then "World"
    }
    String str = list_str[1]; // str == "World"
    list_str.free();        // Free all memory associated with list.
}

Fixed Size Multi-Dimensional Arrays

Declare two-dimensional fixed arrays as <type>[<inner-size>][<outer-size>] arr, like int[4][2] arr. Below you can see how this compares to C:

// C
// Uses: name[<outer-size>][<inner-size>]
int array_in_c[4][2] = {
    {1, 2},
    {3, 4},
    {5, 6},
    {7, 8},
};

// C3
// Uses: <type>[<inner-size>][<outer-size>]
// C3 declares the dimensions, inner-most to outer-most
int[4][2] array = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
};

// To match C we must invert the order of the dimensions
int[2][4] array = {
    {1, 2},
    {3, 4},
    {5, 6},
    {7, 8},
};

// C3 also supports Irregular arrays, for example:
int[][4] array = {
    { 1 },
    { 2, 3 },
    { 4, 5, 6 },
    { 7, 8, 9, 10 },
};

Note

Accessing the multi-dimensional fixed array has inverted array index order compared to when the array was declared.

// Uses: <type>[<inner-size>][<outer-size>]
int[2][4] array = {
    {1, 2},
    {3, 4},
    {5, 6},
    {7, 8},
};

// Access fixed array using: array[<outer-index>][<inner-index>]
int value = array[3][1]; // 8