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.
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
1. What happens when you assign one static array to another, e.g. int[5] b = a;?
2. What does auto s = arr[1 .. 3]; produce?
3. Why can appending with ~= unexpectedly break aliasing between two slices of the same array?
4. Which compiler flag removes the RangeError bounds checks on array indexing and slicing?
5. What is the difference between int[3][4] and int[][]?
Was this page helpful?
You May Also Like
Structs in D
How D's value-type structs work: construction, methods, value-copy semantics, and the features that distinguish them from classes.
Classes in D
How D's reference-type classes work: single inheritance, polymorphism, the Object root, and construction/destruction lifecycle.
Garbage Collection in D
How D's built-in conservative garbage collector manages class instances, dynamic arrays, and closures, and how to control or work around it.
Manual Memory Management in D
How to opt out of D's garbage collector using malloc/free, std.experimental.allocator, and struct-based RAII for deterministic, low-latency memory control.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics