100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Arrays and Slices in D

How D's static arrays, dynamic arrays, and slices differ in memory ownership, mutation, and performance, and how to use them correctly.

Memory & TypesBeginner8 min readJul 10, 2026
Analogies

Arrays and Slices in D

D provides three closely related but distinct sequence abstractions: static arrays, dynamic arrays, and slices. A static array such as int[5] has its length baked into the type itself and is typically allocated inline — on the stack for a local variable, or inside the containing struct or class for a member. A dynamic array such as int[] is a length-and-pointer pair whose backing storage lives on the garbage-collected heap and can grow. A slice is not really a fourth type at all — int[] is itself a slice: a view made of a pointer and a length over some underlying memory, whether that memory belongs to a static array, a dynamic array, or a string literal. Knowing which of these three you are holding at any point in a program is essential to reasoning about ownership, mutation, and performance in D.

🏏

Cricket analogy: A static array is like a fixed XI submitted before a Test match — 11 players, size locked once the toss happens — while a dynamic array is like the full touring squad of 20 that team management can extend by flying in injury replacements.

Static Arrays

A static array is declared with the length as part of the type, for example int[5] scores;, and its size is fixed at compile time and cannot change. Because the length is part of the type, int[5] and int[10] are different, incompatible types. Static arrays have value semantics: assigning one static array to another copies every element, and passing one to a function by value copies the whole thing onto the callee's stack frame unless you pass by ref. This makes small static arrays cheap and predictable — no heap allocation, no garbage collector involvement — but it also means copying a large static array is an O(n) operation that can silently hurt performance if you are not paying attention. Static arrays default-initialize their elements using the element type's .init value, so int[5] arr; gives you five zeros without any extra code.

🏏

Cricket analogy: A static array's fixed size is like the 11 fielding positions on a cricket ground — you cannot add a 12th fielder mid-over — and copying a static array is like re-fielding an identical XI for the second innings, every position duplicated exactly.

Dynamic Arrays

A dynamic array such as int[] values declares only the element type, not a fixed length, and its storage is allocated on the garbage-collected heap. You can grow it with the append operator, values ~= 42;, or by directly setting values.length = 10;, both of which may trigger a reallocation if the current backing block does not have enough spare capacity. D dynamic arrays expose a .capacity property that tells you how many elements can be appended before a reallocation is needed, and std.array's reserve function lets you pre-grow the backing store to avoid repeated reallocations in a tight loop. Because the array reference — pointer plus length — is itself passed by value, two dynamic array variables can point at the same underlying memory; mutating an element through one is visible through the other until one of them is reassigned to a different length or a fresh allocation.

🏏

Cricket analogy: Appending to a dynamic array with ~= is like a franchise adding an overseas replacement to its IPL squad mid-season, and checking .capacity first is like the team management confirming there's a free foreign-player slot before signing anyone new.

Slices as Views

Slicing an existing array with arr[1 .. 3] does not copy any elements; it produces a new (pointer, length) pair that views the same memory as arr, starting at index 1 and stopping before index 3. Inside a slicing expression, the special $ symbol means the length of the array being sliced, so arr[$-1] refers to the last element and arr[0 .. $] is a full-array slice. Because slices are views, mutating slice[0] = 99; after auto slice = arr[1 .. 3]; also changes arr[1], since both refer to the same backing storage — this aliasing is powerful for avoiding copies but is a common source of bugs when a function silently mutates data the caller expected to be read-only. To get an independent copy instead of a view, call .dup for a mutable copy or .idup for an immutable copy, both of which allocate fresh backing memory.

🏏

Cricket analogy: A slice is like a TV broadcaster's highlights package that plays a window of overs 10 through 15 straight from the live match feed, so if the ground's scoreboard updates mid-highlight, the excerpt reflects the same live data rather than a separate recording.

Copying, Concatenation, and Bounds Safety

The ~ operator concatenates two arrays and always allocates a brand-new array for the result, so a ~ b never mutates either operand. The ~= operator appends in place and reuses the existing backing memory when there is spare .capacity, but silently falls back to allocating a new block and copying everything over when there isn't — this is why appending to two slices that alias the same array can suddenly stop aliasing partway through a loop. Bounds checking is enabled by default: indexing or slicing out of range throws a core.exception.RangeError in a normal debug build, though this check is stripped out under the -release compiler flag for maximum performance, so code that relies on bounds errors for correctness must not be shipped with -release unless the invariant is guaranteed some other way. Multidimensional arrays follow the same rules: int[3][4] is a static array of 4 static arrays of 3 ints each, while int[][] is a dynamic array of dynamic arrays, letting each row have an independent, jagged length.

🏏

Cricket analogy: Concatenation with ~ is like a broadcaster splicing together two separate match highlight reels into one brand-new video file, while ~= append is like adding one more over's footage directly onto the end of an existing highlight reel already being edited.

d
import std.stdio;
import std.array : reserve;

void main()
{
    // Static array: fixed length is part of the type
    int[5] fixed = [1, 2, 3, 4, 5];
    int[5] copy = fixed;      // value copy, independent storage
    copy[0] = 99;
    writeln(fixed[0], " ", copy[0]); // 1 99

    // Dynamic array: grows on the GC heap
    int[] dyn;
    dyn.reserve(16);          // pre-grow capacity to avoid reallocations
    foreach (i; 0 .. 5)
        dyn ~= i * i;
    writeln(dyn);             // [0, 1, 4, 9, 16]

    // Slice: a (pointer, length) view, no copy
    int[] view = dyn[1 .. 3];
    view[0] = -1;
    writeln(dyn);             // [0, -1, 4, 9, 16] -- aliasing!

    // Independent copy
    int[] snapshot = dyn.dup;
    snapshot[0] = 1000;
    writeln(dyn[0], " ", snapshot[0]); // 0 1000

    // Concatenation always allocates a new array
    int[] merged = fixed[] ~ dyn;
    writeln(merged.length);   // 10
}

The $ token inside a slicing or indexing expression is shorthand for the array's current length, so arr[$-1] is always the last element and arr[$-3 .. $] is always the last three elements — this reads correctly even after the array has grown or shrunk.

~= can silently break aliasing: if slice and arr originally share memory, appending to arr past its .capacity forces a reallocation, after which slice still points at the old, now-detached backing memory. Never assume two array variables stay aliased across an append.

  • Static arrays (int[5]) have their length fixed in the type, live inline, and are copied by value on assignment.
  • Dynamic arrays (int[]) are a (pointer, length) pair backed by GC heap memory that can grow via ~= or by setting .length.
  • A slice is a view, not a copy — arr[1..3] shares memory with arr, so mutating one can mutate the other.
  • Use .dup for an independent mutable copy and .idup for an independent immutable copy.
  • ~ always allocates a new array; ~= appends in place until .capacity is exhausted, then silently reallocates.
  • Bounds checking throws RangeError by default but is stripped under -release, so out-of-range access becomes undefined behavior in release builds.
  • int[3][4] is a fixed 4-of-3 static grid, while int[][] is a jagged dynamic array of dynamic arrays.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#DProgrammingStudyNotes#ArraysAndSlicesInD#Arrays#Slices#Static#Dynamic#DataStructures#StudyNotes#SkillVeris