← Code Compare

Arrays & Slices

A fixed array is a contiguous block of N elements whose size is part of its type; a slice is a lightweight view into that storage - really just a (pointer, length) pair - that owns nothing. This topic makes the same 5-int array [10,20,30,40,50], takes a view of the middle three [20,30,40], and prints them, so you can see exactly where the array lives, how a slice borrows it without copying, and how each language handles bounds.

Show: CC++HolyCZigHareOdinForth
C
#include <stdio.h>
#include <stddef.h>

int main(void) {
    // Fixed array: 5 ints contiguous on the stack. Its size is the type.
    int a[5] = {10, 20, 30, 40, 50};
    size_t n = sizeof a / sizeof a[0];   // 5, computable only here

    // C has no slice type: a "view" is a raw pointer + a length you carry.
    int  *view = &a[1];                  // points at the middle element
    size_t view_len = 3;                 // [20, 30, 40] -- you track this

    for (size_t i = 0; i < view_len; i++)
        printf("%d\n", view[i]);         // view[i] == a[1 + i]

    // No bounds checks at all: view[3] would read a[4] (or worse) silently.
    return 0;                            // stack array; nothing to free
}

In C an array decays to a bare pointer, losing its length - so a "slice" is just &a[1] plus a size_t length you pass around by hand. There are no bounds checks: indexing past view_len is undefined behaviour, which is exactly why higher languages bundle the length into the slice itself.

C++
#include <array>
#include <span>
#include <iostream>

int main() {
    // std::array<int,5> is a fixed array that knows its size at the type level.
    std::array<int, 5> a{10, 20, 30, 40, 50};

    // std::span (C++20) is a non-owning view: pointer + length, no copy.
    std::span<int> view = std::span{a}.subspan(1, 3); // [20, 30, 40]

    for (int x : view)                    // range-for over the borrowed view
        std::cout << x << '\n';

    // C++20 std::span has no .at(): view[3] is unchecked (UB), like a raw pointer;
    // a.at(4) on the std::array is the bounds-checked accessor that throws.
    return 0;                             // a is on the stack; span owns nothing
}

std::array<int,5> keeps its length in the type, and std::span is the idiomatic borrowed view - a (pointer, size) pair that copies nothing and frees nothing (it does not own the data). subspan(1, 3) carves the middle three; in C++20 std::span has no .at(), so view[i] is unchecked (out-of-bounds is undefined behaviour, exactly like the underlying array) - the bounds-checked, throwing accessor is a.at(i) on the owning std::array (a span gained its own .at() only in C++26).

HolyC
// HolyC has C-style fixed arrays but no slice type or bounds checks.
I64 a[5];
a[0] = 10; a[1] = 20; a[2] = 30; a[3] = 40; a[4] = 50; // fixed, on the stack

// A "view" of the middle three is a bare pointer plus a length you carry.
I64 *view = &a[1];        // points at a[1]
I64 view_len = 3;         // [20, 30, 40] -- tracked by hand

I64 i;
for (i = 0; i < view_len; i++)
  Print("%d\n", view[i]); // view[i] == a[1 + i]

// No bounds checking: view[3] silently reads a[4]. Stack array: no Free needed.

HolyC arrays behave like C's: a fixed I64 a[5] lives on the stack and decays to a pointer that forgets its length, so a slice is simply &a[1] paired with a length you maintain. There are no bounds checks, and since the array is on the stack there is nothing to Free; this only runs under TempleOS.

Zig
const std = @import("std");

pub fn main() void {
    // Fixed array: [5]i32, length is part of the type and known at comptime.
    var a = [5]i32{ 10, 20, 30, 40, 50 };

    // A slice []i32 is a fat pointer: { ptr, len }. a[1..4] borrows a, no copy.
    const view: []i32 = a[1..4];          // [20, 30, 40], view.len == 3

    for (view) |x|                        // iterate the borrowed slice
        std.debug.print("{d}\n", .{x});

    // Slices are bounds-checked in safe builds: a[1..6] or view[3] would panic.
    // No allocator was used, so there is nothing to free.
}

Zig distinguishes the fixed array [5]i32 (size in the type) from a slice []i32, which is a fat pointer carrying ptr and len. Slicing with a[1..4] borrows the array without allocating, and in safe builds both the slice bounds and every index are checked at runtime (out-of-range panics) - no allocator is involved, so nothing needs freeing.

Hare
use fmt;

export fn main() void = {
	// Fixed array: [5]int, its length is part of the type.
	let a: [5]int = [10, 20, 30, 40, 50];

	// A slice []int is a (pointer, length, capacity) view -- borrows a, no copy.
	let view: []int = a[1..4];          // [20, 30, 40], len(view) == 3

	for (let i = 0z; i < len(view); i += 1)
		fmt::println(view[i])!;        // ! aborts on a write error

	// Bounds are checked: a[1..6] or view[3] aborts. view borrows a's
	// stack storage, so there is no alloc and nothing to free.
};

Hare separates the fixed array [5]int from a slice []int, which is a view carrying a pointer, length, and capacity. a[1..4] borrows the array's storage without allocating, len(view) reads the length the slice carries, and indexing is bounds-checked (it aborts on overrun) - because nothing was alloc'd, no free is required.

Odin
package main

import "core:fmt"

main :: proc() {
	// Fixed array: [5]int, length is part of the type, lives on the stack.
	a := [5]int{10, 20, 30, 40, 50}

	// A slice []int is a {data, len} view. a[1:4] borrows a -- no allocation.
	view: []int = a[1:4]                // [20, 30, 40], len(view) == 3

	for x in view {                    // range over the borrowed slice
		fmt.println(x)
	}

	// Bounds-checked by default: a[1:6] or view[3] triggers a runtime panic.
	// No make/new was used, so there is nothing to delete/free.
}

Odin's [5]int is a value-type fixed array, while a slice []int is a {data, len} view; a[1:4] borrows the array's stack storage with no allocation, so no delete is needed. Slice indexing and slicing are bounds-checked by default and panic on overrun (you can disable checks per-build for hot paths).

Forth
\ Forth has no slice type. A fixed array is reserved data space; a "slice"
\ is just a start address plus a length you carry on the stack ( addr len ).

CREATE A  5 CELLS ALLOT        \ fixed array of 5 cells at HERE (static)
10 A 0 CELLS + !   20 A 1 CELLS + !   30 A 2 CELLS + !
40 A 3 CELLS + !   50 A 4 CELLS + !   \ A = [10,20,30,40,50]

: SLICE ( addr i n -- addr' n )  \ view of n cells starting at index i
  >R  CELLS +  R> ;              \ addr' = addr + i*cell ; keep n

: .SLICE ( addr n -- )           \ print n cells from addr
  0 ?DO  DUP @ . CR  CELL+  LOOP  DROP ;

A 1 3 SLICE   \ ( addr+1cell  3 )  -- view of the middle three [20,30,40]
.SLICE        \ prints 20 30 40

\ No bounds checking exists: SLICE with i+n > 5 just reads past A silently.
\ ALLOT space lives until the dictionary is forgotten; it is never freed.

Forth has neither a fixed-array type nor a slice type: CREATE A 5 CELLS ALLOT reserves raw data space, and a "slice" is modelled the same way C does it - an ( addr len ) pair on the stack, here produced by SLICE as base + i*cell with the length kept alongside. There are no bounds checks (an over-long view reads past A), and ALLOT data is static dictionary space that is never individually freed.