Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Language Atlas (mini-courses for more languages)  Module 9 – D for C, C++ and Python programmers

D for C, C++ and Python developers

Slices that share memory, UTF-8 strings, integers that wrap, lazy ranges with UFCS, structs and classes, scope guards, @safe and unittest blocks in D.

  • Beginner
  • 20 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7 and LDC 1.43.0
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Predict when D slices share memory and when .dup or an append copies it
  • Predict what D gives for division, overflow, rounding and string length where C and Python differ
  • Write lazy range pipelines with UFCS and std.algorithm
  • Use structs, classes, scope guards and exceptions, and tell an Exception from an Error
  • Protect release builds with @safe bounds checks, and test code with unittest blocks

Before you start

What you’ll learn · Before you start

What you will learn

  • Predict when D slices share memory and when .dup or an append copies it
  • Predict what D gives for division, overflow, rounding and string length where C and Python differ
  • Write lazy range pipelines with UFCS and std.algorithm
  • Use structs, classes, scope guards and exceptions, and tell an Exception from an Error
  • Protect release builds with @safe bounds checks, and test code with unittest blocks

Before you start

On this page

In D, writing s[0] after s = arr[1 .. 4] changes arr[1] as well, as a pointer into the array would in C, and int.max + 1 is −2,147,483,648 without a word, while the same lines in Python copy the list and give 2,147,483,648. D reads like C and C++ with much of Python’s convenience, and its surprises sit where it behaves like one of them in one place and like another elsewhere.

This lesson runs one small program per idea on LDC 1.43.0: slices and associative arrays, UTF-8 strings, the integer and rounding rows of the concept map: A table of the questions on which languages differ in meaning, such as how whole numbers divide or how a half rounds, answered for each new language with a run of your own. More in “A one-week plan for learning any language”, lazy range pipelines, structs against classes with scope guards and exceptions, and the @safe attribute, which decides whether a release build still checks array bounds.

A slice is a view, and .dup makes a copy

A D array variable holds two things, a pointer to the first element and a length, and arr[1 .. 4] makes another such pair over the same memory: a slice (D): A view of part of an array: a pointer to its first element and a length, written arr[1 .. 4]. Nothing is copied, so writing through the slice changes the array; .dup makes a copy with memory of its own.. Nothing is copied, so writing through either name changes both, as in C with a pointer and a length, and unlike Python’s arr[1:4], which copies. Step through the program and watch which memory each name points to:

Interactive · step by step Three names and the memory they point to Step through slices.d and watch which memory each name points to as the program writes through them.
Three names and the memory they point to, step 7 of 7. Showing arr, s, t, arr's memory (1 · 20 · 3 · 4 · 5), s's new memory (7 · 3 · 4 · 40), t's own copy (99 · 3 · 4). In focus: s's new memory.all 5all 4all 3arrstarr's memory1 · 20 · 3 · 4 · 5s's new memory7 · 3 · 4 · 40t's own copy99 · 3 · 4

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. One array. int[] arr = [1, 2, 3, 4, 5] allocates memory for five ints and makes arr point to it. An array variable holds two things: where the data starts and how many elements there are.
  2. A slice. s = arr[1 .. 4] copies nothing. s points into the same memory, at element 1, with a length of 3: the elements 2, 3 and 4.
  3. Writing through s. s[0] = 20 writes element 1 of the shared memory, so arr changes too: the session printed arr = [1, 20, 3, 4, 5] and s = [20, 3, 4].
  4. A copy. t = s.dup allocates new memory and copies the three elements into it. t[0] = 99 changes only that copy, and arr stays [1, 20, 3, 4, 5].
  5. Room to grow. arr.capacity is 7: its memory has room for two more elements after arr's five. s.capacity is 0, because s ends at element 3 while element 4 still belongs to arr.
  6. An append that copies. s ~= 40 cannot grow s in place without writing over arr's element 4, so the runtime copies s's three elements to new memory first and appends 40 there.
  7. Two arrays again. s[0] = 7 now changes only the new memory: the session printed s = [7, 3, 4, 40] and arr still [1, 20, 3, 4, 5].

Here is the program, with a second one about associative arrays, and the session that ran both:

slices.d: one array, a slice, a copy and an append D · slices/slices.d
import std.stdio;

void main()
{
    int[] arr = [1, 2, 3, 4, 5];
    int[] s = arr[1 .. 4];       // a view of arr[1], arr[2] and arr[3]
    s[0] = 20;
    writeln("arr = ", arr, "  s = ", s);

    int[] t = s.dup;             // a copy with memory of its own
    t[0] = 99;
    writeln("arr = ", arr, "  t = ", t);

    writeln("s.capacity = ", s.capacity, ", arr.capacity = ", arr.capacity);
    s ~= 40;                     // s does not end where arr's data ends: the runtime copies first
    s[0] = 7;
    writeln("arr = ", arr, "  s = ", s);

    int[] small = [1, 2, 3];
    writeln(small[5]);
}
stock.d: an associative array D · slices/stock.d
import std.stdio;

void main()
{
    int[string] stock = ["pens": 12, "pads": 3];
    stock["clips"] = 40;
    writeln(stock.length, " keys; ", "pens" in stock ? "pens are in stock" : "no pens");
    if (auto count = "pads" in stock)    // in gives a pointer to the value, or null
        *count += 1;
    writeln("pads: ", stock["pads"], ", ink: ", stock.get("ink", 0));
    writeln(stock["ink"]);
}
slices.d and stock.d built and run Python · slices_session.py
from session import run

# Slices of one array, a copy, an append that copies, and an index past the end; then an associative array, a lookup
# with in and with get, and a key that is not there.
run("""
cd slices
ldc2 slices.d
!./slices
ldc2 stock.d
!./stock
""")

Output

$ cd slices
$ ldc2 slices.d
$ ./slices
arr = [1, 20, 3, 4, 5]  s = [20, 3, 4]
arr = [1, 20, 3, 4, 5]  t = [99, 3, 4]
s.capacity = 0, arr.capacity = 7
arr = [1, 20, 3, 4, 5]  s = [7, 3, 4, 40]
core.exception.ArrayIndexError@slices.d(20): index [5] is out of bounds for array of length 3
[exit status 1]
$ ldc2 stock.d
$ ./stock
3 keys; pens are in stock
pads: 4, ink: 0
core.exception.RangeError@stock.d(11): Range violation
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 slices_session.py

.dup gave t memory of its own, so t[0] = 99 left arr alone. The append is the subtle line. s.capacity was 0, because s ends where arr still has an element, so s ~= 40 could not grow in place without writing over arr[4]: the runtime copied s first, and from then on the two were separate arrays. Whether names still share memory after an append depends on a capacity that the code does not show, so take a .dup whenever you mean a copy.

Indexing is checked: small[5] stopped the program with core.exception.ArrayIndexError, naming the index, 5, and the length, 3, where C would read whatever lies there. Associative arrays, D’s built-in hash tables, follow the same rule. in gives a pointer to the value, or null, get gives a default, and reading a missing key, stock["ink"], threw a RangeError: the counterpart of Python’s KeyError, but an Error, which the section on classes comes back to.

Expecting a slice to be a copy

A Python habit writes part = arr[1 .. 4], changes part and expects arr to stay as it was. In D both change, as s[0] = 20 did above. Write arr[1 .. 4].dup when you need a copy.

Checkpoint

Check: Slices, copies and associative arrays

  1. Question 1 of 2 arr is [1, 2, 3, 4, 5] and s = arr[1 .. 4]. After s[0] = 20, what is arr?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: [1, 20, 3, 4, 5]

    A slice is a pointer and a length over the same memory. Writing through it writes the array, which is why the session printed arr = [1, 20, 3, 4, 5] next to s = [20, 3, 4].

  2. Question 2 of 2 s.capacity is 0. After s ~= 40 and then s[0] = 7, what happens to arr?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Nothing: the append copied s to new memory, so s[0] = 7 writes only the copy

    An append grows a slice in place only when its memory has room after it that no other array uses. s.capacity was 0, so s ~= 40 copied s first, and arr stayed [1, 20, 3, 4, 5].

A string is UTF-8, and length counts bytes

string is a slice too: immutable(char)[], an array of UTF-8 code unit: The unit in which an encoding stores text: one byte in UTF-8, two in UTF-16. ï takes two UTF-8 code units and 😀 four, and a D string's length counts UTF-8 code units. that cannot be changed through it. So length counts bytes, not letters, and the four strings of this program show where the three ways of counting part:

strings.d: four strings, three ways to count D · strings/strings.d
import std.stdio;
import std.range : walkLength;
import std.uni : byGrapheme;

void main()
{
    string city = "Pune";
    string word = "naïve";
    string face = "😀";
    string flag = "🇮🇳";
    writeln("length:     ", city.length, " ", word.length, " ", face.length, " ", flag.length);
    writeln("walkLength: ", city.walkLength, " ", word.walkLength, " ", face.walkLength, " ", flag.walkLength);
    writeln("graphemes:  ", city.byGrapheme.walkLength, " ", word.byGrapheme.walkLength, " ", face.byGrapheme.walkLength, " ", flag.byGrapheme.walkLength);
    writeln(typeof(word).stringof, ", ", typeof(word[0]).stringof);
    foreach (dchar c; word)
        write(c, '|');
    writeln();
    foreach (char c; word)
        write(cast(int) c, ' ');
    writeln();
    writeln(word[0 .. 3], " ", word[0 .. 4]);
    writeln("abc" == "abc", " ", "abc" < "abd", " ", "Zebra" < "apple");
}
strings.d run Python · strings_session.py
from session import run

run("""
cd strings
ldc2 -run strings.d
""")

Output

$ cd strings
$ ldc2 -run strings.d
length:     4 6 4 8
walkLength: 4 5 1 2
graphemes:  4 5 1 1
string, immutable(char)
n|a|ï|v|e|
110 97 195 175 118 101
na� naï
true true true

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 strings_session.py

String length, UTF-8 units walkLength, code points byGrapheme, characters seen
Pune 4 4 4
naïve 6 5 5
😀 4 1 1
🇮🇳, the flag of India 8 2 1

walkLength walks the string as a range, and Phobos’s range functions decode a string into code points, dchar, as foreach (dchar c; word) did when it printed the five letters of naïve. The flag is two code points that a reader sees as one character, a grapheme, which only byGrapheme counts. The foreach (char c; …) line shows the bytes: ï is 195 175. Indexing and slicing work on code units, so word[0 .. 3] cut ï in half and the terminal showed the leftover byte as �, while word[0 .. 4] kept it whole. Comparison goes by code unit as well: "Zebra" < "apple" is true, because capital letters come before small ones in UTF-8, as in ASCII.

Unicode Character Inspector Paste naïve or 🇮🇳 to see each character's code points and UTF-8 bytes, the numbers that length and walkLength count.

A limit of 20 characters checked with length

name.length <= 20 allows 20 bytes. A name in Devanagari takes three bytes a code point, so a name of seven code points, 21 bytes, is already refused. Count with walkLength for code points, or byGrapheme for what a reader sees.

Checkpoint

Check: UTF-8 strings and what length counts

  1. Question 1 of 2 What does "naïve".length give in D?

    Type a number.

    A hint

    Count the bytes, not the letters.

    Show the answer: Question 1 of 2

    Answer: 6 code units

    A string is an array of UTF-8 code units, and ï takes two of them, 195 and 175, so the five letters of naïve need six. walkLength gives 5, the code points.

  2. Question 2 of 2 For the flag 🇮🇳, length gave 8 and walkLength 2. Which count matches what a reader sees, and how do you get it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: One character, counted with byGrapheme

    length counts UTF-8 bytes, walkLength code points and byGrapheme the characters a reader sees: 8, 2 and 1 for the flag.

Integers divide like C and wrap without a word

Here are the division, rounding and overflow rows of the concept map, answered for D. The D specification says that for integers the quotient rounds towards zero and the remainder takes the sign of the dividend, as in C, and that addition wraps: int.max + 1 == int.min. The program checks both, and a few neighbours:

numbers.d: division, overflow, rounding and printing D · numbers/numbers.d
import std.stdio;
import std.math : lrint, round;
import core.checkedint : adds;

void main()
{
    writeln("-7 / 2 = ", -7 / 2, ", -7 % 2 = ", -7 % 2, ", 7 / 2 = ", 7 / 2, ", 7.0 / 2 = ", 7.0 / 2);

    int big = int.max;
    big += 1;
    writeln("int.max + 1 = ", big);
    uint none = 0;
    none -= 1;
    writeln("0u - 1 = ", none);
    bool overflow;
    immutable checked = adds(int.max, 1, overflow);
    writeln("adds(int.max, 1) = ", checked, ", overflow: ", overflow);

    writeln("round(2.5) = ", round(2.5), ", round(-2.5) = ", round(-2.5), ", lrint(2.5) = ", lrint(2.5));
    writeln("0.1 + 0.2 = ", 0.1 + 0.2, ", == 0.3: ", 0.1 + 0.2 == 0.3);
    writefln("0.1 + 0.2 = %.17g", 0.1 + 0.2);

    byte b = 100;
    auto sum = b + b;
    writeln("byte + byte is ", typeof(sum).stringof, ": ", sum);
}
numbers.d run, and a conversion D refuses Python · numbers_session.py
from session import run

run("""
cd numbers
ldc2 -run numbers.d
!ldc2 narrow.d
""")

Output

$ cd numbers
$ ldc2 -run numbers.d
-7 / 2 = -3, -7 % 2 = -1, 7 / 2 = 3, 7.0 / 2 = 3.5
int.max + 1 = -2147483648
0u - 1 = 4294967295
adds(int.max, 1) = -2147483648, overflow: true
round(2.5) = 3, round(-2.5) = -3, lrint(2.5) = 2
0.1 + 0.2 = 0.3, == 0.3: false
0.1 + 0.2 = 0.30000000000000004
byte + byte is int: 200
$ ldc2 narrow.d
narrow.d(4): Error: cannot implicitly convert expression `total` of type `int` to `byte`
    byte small = total;
                 ^
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 numbers_session.py

Probe D, as recorded Python
-7 / 2 and -7 % 2 -3 and -1 -4 and 1, with // and %
int.max + 1 -2147483648, by the rules 2147483648, no limit
round(2.5), round(-2.5) 3 and -3 2 and -2
0.1 + 0.2, printed 0.3 with writeln 0.30000000000000004

The wrap is defined behaviour in D, so every compiler gives the same −2,147,483,648, while in C a signed overflow is undefined behaviour. When you need to know about it, core.checkedint tells you: adds returned the same wrapped sum and set its overflow flag to true, and an unsigned 0 − 1 is 4,294,967,295. round takes a half away from zero, as C’s round does, while lrint rounds in the current mode, to the even neighbour by default, so lrint(2.5) is 2, what Python’s round gives.

The last two lines are traps for a Python programmer. writeln prints a double with six significant digits, so 0.1 + 0.2 shows as 0.3 while comparing it with 0.3 gives false; %.17g shows all of it, 0.30000000000000004. And byte + byte is an int, which does not narrow to a byte by itself: byte small = total; was refused at compile time. Write cast(byte) total when you know the value fits, or to!byte(total), which throws when it does not.

Checkpoint

Check: Division, overflow, rounding and printing

  1. Question 1 of 2 What does D give for int.max + 1?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: -2147483648: integer addition wraps, by the specification

    D wraps integer addition and defines it, so the result is −2,147,483,648 on every compiler. adds(int.max, 1) gave the same sum and set its overflow flag to true.

  2. Question 2 of 2 writeln(0.1 + 0.2) printed 0.3. Is 0.1 + 0.2 == 0.3 in D?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: No: writeln prints six significant digits, and the sum is 0.30000000000000004

    %s, which writeln uses, prints a double like %g with six significant digits, so 0.30000000000000004 shows as 0.3. Compare with a tolerance, or print with %.17g when the digits matter.

Ranges and UFCS build lazy pipelines

A Phobos algorithm takes a range (D): An object that hands out values one at a time through empty, front and popFront. Phobos's algorithms take and return ranges, and most of them are lazy: nothing is computed until a value is taken., something that hands out values one at a time, and most of them return another range without computing anything yet. With UFCS: Uniform Function Call Syntax: in D, a call f(a, b) may be written a.f(b), so that calls chain from left to right, as in iota(1, 11).filter!(x => x % 2 == 0)., f(a, b) can be written a.f(b), so a pipeline of calls reads from left to right:

pipeline.d: a lazy pipeline, and proof that it is lazy D · ranges/pipeline.d
import std.stdio;
import std.algorithm : filter, map, sum;
import std.array : array;
import std.range : iota, take;

int square(int x)
{
    writeln("  square(", x, ")");
    return x * x;
}

void main()
{
    auto evens = iota(1, 11).filter!(x => x % 2 == 0).map!(x => x * x);
    writeln(evens);
    writeln(iota(1, 101).sum, " ", sum(iota(1, 101)));

    writeln("building a pipeline over a billion numbers");
    auto squares = iota(1, 1_000_000_000).map!square;
    writeln("taking two");
    writeln(squares.take(2).array);
}

iota(1, 11).filter!(x => x % 2 == 0).map!(x => x * x) is three nested calls, and the ! passes each lambda as a template argument, so the compiler builds a filter and a map for those lambdas. Step through how writeln pulls the values out of it:

Interactive · stage by stage One value at a time through a lazy pipeline Step through how writeln pulls the first values out of iota, filter and map, one value at a time.
One value at a time through a lazy pipeline, step 8 of 8. Showing writeln (prints ]), map!(x => x * x) (4 → 16), filter!(x % 2 == 0) (3 dropped, 4 passes), iota(1, 11) (empty after 10). In focus: writeln.writelnprints ]map!(x => x * x)4 → 16filter!(x % 2 == 0)3 dropped, 4 passesiota(1, 11)empty after 10

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. Nothing has run. The line that builds evens computes nothing. It wraps iota in a filter and the filter in a map, and each wrapper only remembers what to do.
  2. writeln asks. writeln needs a first value, so it asks map for its front. map has none of its own, so it asks filter, and filter asks iota.
  3. iota gives 1. iota hands out 1, its first number. It works each number out when asked; it has no list of ten numbers stored anywhere.
  4. filter skips 1. 1 % 2 is 1, so filter drops 1 and asks iota again, before map or writeln hear anything.
  5. iota gives 2. iota hands out 2. 2 % 2 is 0, so filter passes 2 up to map.
  6. map squares it. map applies its lambda to 2 and hands 4 to writeln, which prints it: the first value of [4, 16, 36, 64, 100].
  7. The next value. Asked again, the chain repeats: iota gives 3, which filter drops, then 4, which passes, and map hands 16 to writeln.
  8. The end. After 10, iota is empty, so filter and map are empty too, and writeln closes the list. Each number went up the chain once, only when asked for.

pipeline.d and topwords.d run, and one import missing Python · ranges_session.py
from session import run

run("""
cd ranges
ldc2 -run pipeline.d
ldc2 -run topwords.d
!ldc2 -c noempty.d
""")

Output

$ cd ranges
$ ldc2 -run pipeline.d
[4, 16, 36, 64, 100]
5050 5050
building a pipeline over a billion numbers
taking two
  square(1)
  square(2)
[1, 4]
$ ldc2 -run topwords.d
cat 3; the 3; hat 2;
ek 3; do 2; teen 1;
café 3; naïve 1;
$ ldc2 -c noempty.d
noempty.d(7): Error: no property `empty` for type `string`, perhaps `import std.range;` is needed?
    foreach (word; "a cat's hat".splitter!(c => !c.isAlpha).filter!(w => !w.empty))
                                                                           ^
…/bin/../import/std/algorithm/iteration.d(1506):        instantiated from here: `FilterResult!(__lambda_L7_C69, SplitterResult!(__lambda_L7_C44, string))`
        return FilterResult!(unaryFun!predicate, Range)(range);
               ^
noempty.d(7):        instantiated from here: `filter!(SplitterResult!(__lambda_L7_C44, string))`
    foreach (word; "a cat's hat".splitter!(c => !c.isAlpha).filter!(w => !w.empty))
                                                           ^
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ranges_session.py

The second half of pipeline.d shows the laziness: a map over a billion numbers was built at once, and take(2) made square run exactly twice. The same rule of UFCS makes iota(1, 101).sum and sum(iota(1, 101)) one call, 5,050 both times. topwords.d puts the pieces together:

topwords.d: the most frequent words of a text D · ranges/topwords.d
import std.stdio;
import std.algorithm : filter, multiSort, splitter;
import std.array : array;
import std.range : empty, take;
import std.uni : isAlpha, toLower;

/// The k most frequent words of text. A word is a run of letters, compared in lower case; more frequent words come
/// first, and words with the same count in alphabetical order.
auto topWords(string text, size_t k)
{
    int[string] counts;
    foreach (word; text.splitter!(c => !c.isAlpha).filter!(w => !w.empty))
        counts[word.toLower]++;
    auto pairs = counts.byKeyValue.array;
    pairs.multiSort!("a.value > b.value", "a.key < b.key");
    return pairs.take(k);
}

void main()
{
    foreach (text; ["The cat and the hat. The CAT sat; a cat's hat!", "Ek, do, teen: ek do. Ek!", "Café, café, CAFÉ; naïve"])
    {
        foreach (p; topWords(text, 3))
            write(p.key, " ", p.value, "; ");
        writeln();
    }
}

splitter!(c => !c.isAlpha) cuts the text at every character that is not a letter, so cat's gives cat and s, and filter drops the empty pieces between two separators. The associative array counts, byKeyValue.array turns its pairs into an array, and multiSort orders them by count and then by word, which is why cat came before the at 3 each. Note the line import std.range : empty, take;: .empty on a string is a function of Phobos, not of the language, and noempty.d, the same pipeline without that import, failed with “no property empty for type string”, the compiler suggesting import std.range; itself.

Ask for the values you need

A lazy range does its work when a value is taken, so a pipeline with take(10) at the end does the work of ten values, as take(2) did two squares. Call .array only where you need all the values at once, as topwords.d does before sorting.

Checkpoint

Check: Lazy ranges and UFCS

  1. Question 1 of 2 squares = iota(1, 1_000_000_000).map!square, then squares.take(2).array. How many times does square run?

    Type a number.

    A hint

    Count the lines square(…) in the session's output.

    Show the answer: Question 1 of 2

    Answer: 2 times

    map is lazy: building the pipeline ran nothing, and take(2) asked for two values, so square printed twice, square(1) and square(2).

  2. Question 2 of 2 Which call is the same as iota(1, 101).sum?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: sum(iota(1, 101))

    With UFCS, an ordinary function f(a, b) can be called as a.f(b). That is how a pipeline of functions such as filter, map and sum reads from left to right.

Structs are values, classes are references

D has two kinds of user type, and each behaves like one of its neighbours. A struct is a value, as a C++ object is: assigning or passing it copies its fields. A class object lives on the garbage-collected heap, as in Java, and a class variable is a reference to it:

Case struct Point class Box
b = a copies the fields a second name for one object
Passed to a function the function gets a copy the function changes your object
== compares the fields compares identity, unless the class defines opEquals
Never assigned its fields’ default values null
values.d: copies, references and scope guards D · values/values.d
import std.stdio;

struct Point { int x, y; }

class Box
{
    int size;
    this(int size) { this.size = size; }
}

void scaled(Point p) { p.x *= 10; }
void grown(Box b) { b.size *= 10; }

void main()
{
    Point a = Point(1, 2);
    Point b = a;            // a copy of the struct
    b.x = 9;
    scaled(a);              // scaled changes its own copy
    writeln("a = ", a, ", b = ", b);

    Box p = new Box(1);
    Box q = p;              // a second reference to the same object
    q.size = 9;
    grown(p);
    writeln("p.size = ", p.size, ", q.size = ", q.size, ", p is q: ", p is q);
    writeln("Point(1, 2) == Point(1, 2): ", Point(1, 2) == Point(1, 2), "; new Box(1) == new Box(1): ", new Box(1) == new Box(1));

    {
        scope (exit) writeln("3. scope(exit): leaving the block");
        scope (success) writeln("2. scope(success): no exception");
        writeln("1. in the block");
    }
    try
    {
        scope (failure) writeln("scope(failure): an exception is leaving");
        throw new Exception("bad mark");
    }
    catch (Exception e)
        writeln("caught: ", e.msg);
}
values.d, errors.d and nobox.d run Python · values_session.py
from session import run

run("""
cd values
ldc2 -run values.d
ldc2 errors.d
!./errors
ldc2 nobox.d
!./nobox
""")

Output

$ cd values
$ ldc2 -run values.d
a = Point(1, 2), b = Point(9, 2)
p.size = 90, q.size = 90, p is q: true
Point(1, 2) == Point(1, 2): true; new Box(1) == new Box(1): false
1. in the block
2. scope(success): no exception
3. scope(exit): leaving the block
scope(failure): an exception is leaving
caught: bad mark
$ ldc2 errors.d
$ ./errors
ConvException: Unexpected 'a' when converting from type string to type int
core.exception.ArrayIndexError@errors.d(13): index [3] is out of bounds for array of length 3
[exit status 1]
$ ldc2 nobox.d
$ ./nobox
none is null: true
[exit status 139]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 values_session.py

scaled(a) changed only its own copy, so a stayed Point(1, 2), while grown(p) changed the one Box that p and q both name. Two separately made boxes were not ==, because the default opEquals of a class is identity, the same test as is. A class variable that was never assigned is null, and using it ended nobox with exit status 139, a crash on signal 11, not an exception that a handler could catch.

scope(exit), scope(success) and scope(failure) are scope guard: A statement scope(exit), scope(success) or scope(failure) that runs when the enclosing block ends: always, only without an exception, or only while one leaves. Several run in reverse order, the last written first.: each runs its statement when the block ends, always, only without an exception, or only while one leaves. Several run in reverse order, the last written first, as the lines numbered 2 and 3 show, which lets the clean-up sit next to what it cleans.

errors.d shows the line between the two kinds of throwable. to!int("12a") threw a ConvException, which the program caught. marks[3] threw an ArrayIndexError, which catch (Exception e) does not catch: it is an Error (D): A throwable for a failure a program is not expected to recover from, such as ArrayIndexError, RangeError or AssertError. catch (Exception e) does not catch it, and the documentation calls it not safe to catch., which the documentation of object calls unrecoverable and not safe to catch, so the program stopped. Catch exceptions, and fix the code that raises errors.

Checkpoint

Check: Structs, classes, scope guards and errors

  1. Question 1 of 2 Point is a struct and Box a class. a = Point(1, 2); scaled(a) multiplies p.x by 10 inside. p = new Box(1); grown(p) does the same to size. What do a.x and p.size become?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: a.x stays 1 and p.size becomes 10

    In the session a stayed Point(1, 2) after scaled, while grown multiplied the size that q had set to 9, so p.size and q.size were both 90: one object, two names.

  2. Question 2 of 2 Inside try ... catch (Exception e), marks[3] is read on a three-element array. What happens?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The ArrayIndexError passes the handler and stops the program

    D separates Exceptions, which a program may catch and handle, from Errors such as ArrayIndexError, RangeError and AssertError, which the documentation calls unrecoverable and not safe to catch.

@safe keeps bounds checks in a release build

@safe marks a function whose memory safety the compiler checks: among other rules, it may not do pointer arithmetic or call a function that is @system, the default. Here a struct has a toString, and main is @safe:

marks.d: a @safe main and a struct's toString D · safety/marks.d
import std.stdio;
import std.format : format;

struct Mark
{
    string name;
    int score;

    string toString() const
    {
        return format("%s: %d", name, score);
    }
}

void main() @safe
{
    auto m = Mark("Asha", 91);
    writeln(m);
}
@safe, bounds checks under --release, and a unittest block Python · safety_session.py
from session import run

# @safe and a struct's toString; what --release does to bounds checks, read from ldc2 --help and then tried on one
# program built twice; a unittest block that fails, and the fix. The lines of the long compiler error that show the
# chain of calls are picked out with grep.
run("""
cd safety
ldc2 marks.d 2>&1 | grep -e 'Error' -e 'which calls'
ldc2 -run marks_fixed.d
ldc2 --help | grep -e '--release '
ldc2 bounds.d -of=bounds_debug
!./bounds_debug
!./bounds_debug safe
ldc2 --release bounds.d -of=bounds_release
./bounds_release
!./bounds_release safe
ldc2 -unittest -main grades.d -of=grades_test
!./grades_test
ldc2 -unittest -main grades_fixed.d -of=grades_fixed
./grades_fixed
""")

Output

$ cd safety
$ ldc2 marks.d 2>&1 | grep -e 'Error' -e 'which calls'
marks.d(18): Error: `@safe` function `D main` cannot call `@system` function `std.stdio.writeln!(Mark).writeln`
…/bin/../import/std/stdio.d(1663):        which calls `write`
…/bin/../import/std/format/write.d(527):        which calls `formattedWrite`
…/bin/../import/std/format/write.d(1255):        which calls `formatValue`
…/bin/../import/std/format/internal/write.d(2522):        which calls `formatValueImpl`
…/bin/../import/std/format/internal/write.d(2157):        which calls `formatObject`
marks.d(9):        which calls `toString`
$ ldc2 -run marks_fixed.d
Asha: 91
$ ldc2 --help | grep -e '--release '
  --release                                    - Compile release version, defaulting to disabled asserts/contracts/invariants, and bounds checks in @safe functions only
$ ldc2 bounds.d -of=bounds_debug
$ ./bounds_debug
core.exception.ArrayIndexError@bounds.d(3): index [3] is out of bounds for array of length 3
[exit status 1]
$ ./bounds_debug safe
core.exception.ArrayIndexError@bounds.d(4): index [3] is out of bounds for array of length 3
[exit status 1]
$ ldc2 --release bounds.d -of=bounds_release
$ ./bounds_release
@system read: 61
$ ./bounds_release safe
core.exception.ArrayIndexError@bounds.d(4): index [3] is out of bounds for array of length 3
[exit status 1]
$ ldc2 -unittest -main grades.d -of=grades_test
$ ./grades_test
grades.d(13): [unittest] 75 is a B
1/1 modules FAILED unittests
[exit status 1]
$ ldc2 -unittest -main grades_fixed.d -of=grades_fixed
$ ./grades_fixed
1 modules passed unittests

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 safety_session.py

The first command fails, and the reason is attribute inference: The compiler working out attributes such as @safe, pure, nothrow and @nogc from a function's body. D does it for templates, function literals, auto functions and nested functions, and not for other functions.. writeln is a template, so the compiler works out its attributes from its body, and they would be @safe; but it calls Mark’s toString, an ordinary member function. The specification infers attributes for function literals, auto functions, nested functions and templates only, so toString counts as @system, and the message follows the chain of calls from writeln down to it. Marking toString as @safe fixed the build, and marks_fixed.d printed Asha: 91.

Why bother with @safe? Because of the next lines. ldc2 --help says that --release switches off asserts, contracts and invariants, and keeps bounds checks in @safe functions only. bounds.d reads one element past the end of a three-element slice, once through a @system function and once through a @safe one:

Build of bounds.d @system read @safe read
ldc2 bounds.d ArrayIndexError ArrayIndexError
ldc2 --release bounds.d printed 61 and went on ArrayIndexError

The 61 is the fourth element of the array the slice was cut from, read without a word. dub’s release build type is release mode, so the same holds for dub build --build=release.

Key idea

In a release build only @safe functions keep their bounds checks, so write the code that indexes arrays as @safe, and give a struct’s toString the attribute too when @safe code prints it.

unittest blocks test the code beside it. -unittest compiles them in and -main adds an empty main, so ldc2 -unittest -main grades.d makes a program that only runs the tests. Its first run reported grades.d(13): [unittest] 75 is a B, with the message of the failing assert; with >= in place of >, the second reported 1 modules passed unittests.

Checkpoint

Check: @safe, release builds and unittest blocks

  1. Question 1 of 2 bounds.d was built with ldc2 --release. What did the @system read one past the end of the slice do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It printed 61, the next element of the array, and the program went on

    ldc2 --help describes --release as disabling asserts, contracts and invariants, and bounds checks except in @safe functions. The debug build stopped both reads; the release build stopped only the @safe one.

  2. Question 2 of 2 A @safe main calls writeln(m) for a struct whose toString has no attributes, and the build fails. Why is writeln not the problem?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: writeln is a template, so its attributes are inferred; it is toString, an ordinary member function, that counts as @system

    The specification infers attributes for templates, function literals, auto functions and nested functions only. writeln is a template and would be @safe, but it calls toString, an ordinary function without attributes, so the chain is @system.

Practise: port topWords to Python

A second implementation is a good test of a first: the exercise asks for topWords of topwords.d in Python, giving the same pairs as the D program. Its sample tests are the three texts of the session, with the lists that topwords.d printed, and a few cases of its rules.

Exercise · Easy · Python

Port the topWords range pipeline from D to Python

topwords.d in this lesson counts words with an associative array and sorts them with multiSort. Port its topWords to Python, in topwords.py, so that it gives the same result as the D program for every text:

  • top_words(text, k) returns a list of (word, count) pairs, at most k of them.
  • A word is a run of letters, as splitter!(c => !c.isAlpha) cuts the text: every character that is not a letter ends a word, so cat's gives cat and s, and digits are not part of words. In Python, str.isalpha() tells a letter for every text of the tests.
  • Words are counted in lower case, as toLower makes them: The and the are one word.
  • More frequent words come first, and words with the same count come in alphabetical order, as multiSort!("a.value > b.value", "a.key < b.key") sorts them.

The starter splits at spaces and sorts by count only, the way a first port often does. The sample tests replay the three texts of the D session, and their expected lists are what topwords.d printed.

Starter code · topwords.py

def top_words(text, k):
    """The k most frequent words of text, as topwords.d finds them."""
    counts = {}
    for word in text.split():
        counts[word] = counts.get(word, 0) + 1
    pairs = sorted(counts.items(), key=lambda pair: pair[1], reverse=True)
    return pairs[:k]
The sample tests · test_topwords.py
from topwords import top_words


def test_the_first_text_of_the_session():
    """topwords.d printed cat 3; the 3; hat 2;"""
    assert top_words("The cat and the hat. The CAT sat; a cat's hat!", 3) == [("cat", 3), ("the", 3), ("hat", 2)]


def test_words_in_another_language():
    """topwords.d printed ek 3; do 2; teen 1;"""
    assert top_words("Ek, do, teen: ek do. Ek!", 3) == [("ek", 3), ("do", 2), ("teen", 1)]


def test_letters_beyond_ascii():
    """topwords.d printed café 3; naïve 1; with only two words to give"""
    assert top_words("Café, café, CAFÉ; naïve", 3) == [("café", 3), ("naïve", 1)]


def test_ties_are_alphabetical():
    """four words once each, in alphabetical order"""
    assert top_words("to be or not", 4) == [("be", 1), ("not", 1), ("or", 1), ("to", 1)]


def test_digits_and_apostrophes_end_words():
    """r2d2 gives r and d; it's gives it and s"""
    assert top_words("r2d2 it's it", 3) == [("it", 2), ("d", 1), ("r", 1)]


def test_nothing_to_count():
    """an empty text, a text without letters, and k = 0"""
    assert top_words("", 3) == []
    assert top_words("42, 7; 108!", 3) == []
    assert top_words("the cat", 0) == []
A hint

Collect the letters of the current word in a list and finish the word at every character that is not a letter, and at the end of the text. For the order, sort with a key that puts the count first, negated, and the word second.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Keyword Density Checker See how often each word of your own test text occurs before you work out what top_words should return for it.

Then the self-check, on the six ideas of this lesson. The idea you have answered least well comes up first, and each answer is explained when you pick it:

Practice that adapts to you

Practise this lesson

12 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 What does t = s.dup give t?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: A copy of s's elements in memory of its own

    .dup is the way to say "copy" in D: the session's t = s.dup took [20, 3, 4], and changing t left arr alone.

  2. Question 2 of 12 You port Python code that uses counts.get(word, 0) and counts[word] on a dict. Which D lines behave like them on an associative array?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: counts.get(word, 0) as it is, and counts[word], which throws a RangeError for a missing key

    D's associative arrays mirror Python dicts: get for a default, in for a lookup (a pointer here), and a RangeError, an Error rather than an exception, for a missing key.

  3. Question 3 of 12 A program shortens titles with title[0 .. 20]. On some Hindi titles the output ends in �. Why?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: The slice counts UTF-8 bytes, so position 20 can fall inside a character

    Slice by code points or graphemes, for instance by taking from the range with take or from byGrapheme, and never by a byte index that may land inside a character.

  4. Question 4 of 12 What does "Zebra" < "apple" give in D?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: true

    String comparison is by code unit, which in UTF-8 keeps the order of code points: every capital letter comes before every small one, as in ASCII.

  5. Question 5 of 12 What does D give for -7 % 2?

    Type a number.

    A hint

    The quotient rounds towards zero.

    Show the answer: Question 5 of 12

    Answer: -1

    The remainder takes the sign of the dividend, as in C: -7 / 2 is -3 and -7 % 2 is -1. Python's -7 % 2 is 1, because Python floors the quotient.

  6. Question 6 of 12 A Python report rounds 2.5 to 2 with round(). Which D call gives the same result, by default?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: lrint(2.5), which rounds to the even neighbour in the default mode

    Python's round goes to the even neighbour on a half, and so does D's lrint in the default rounding mode; D's round goes away from zero, as C's does.

  7. Question 7 of 12 auto evens = iota(1, 11).filter!(x => x % 2 == 0).map!(x => x * x); What has been computed after this line?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: Nothing yet: the line builds a filter and a map that compute when asked

    Building a pipeline costs nothing; the work happens one value at a time as a consumer such as writeln, array or sum takes values, as the step-through showed.

  8. Question 8 of 12 Without import std.range, a line with w.empty on a string failed: no property empty for type string. Why?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: empty for arrays is a function of Phobos, which UFCS calls with a dot only once it is imported

    The range primitives of arrays, empty, front and popFront, live in std.range (std.range.primitives). UFCS makes w.empty work only when the function is visible.

  9. Question 9 of 12 A block has scope(exit) A, then scope(exit) B. It ends normally. In which order do they run?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: B, then A

    Scope guards run in reverse lexical order, which keeps clean-up in the reverse order of the work, as with destructors in C++.

  10. Question 10 of 12 Box none; writeln(none.size); What happened when nobox ran?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: It crashed with signal 11, exit status 139, after printing none is null: true

    A struct always exists with its default fields, but a class variable is null until it is assigned. Test with none is null before using it.

  11. Question 11 of 12 A service is built with dub build --build=release and parses arrays in @system code. What do you change so that a bad index still stops it?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: Mark the parsing functions @safe, so their bounds checks stay in the release build

    In a release build only @safe functions are checked, so @safe is what keeps a bad index from reading neighbouring memory, as the 61 in the session did.

  12. Question 12 of 12 What do -unittest and -main do in ldc2 -unittest -main grades.d?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: -unittest compiles the unittest blocks in, -main adds an empty main so the program can run

    unittest blocks are compiled only with -unittest, and run before main. With -main, a module without main becomes a program that only runs its tests.

Interview questions

Warm-up (fresher to mid-level): In D, what does arr[1 .. 4] give you, and when does it stop sharing memory with arr?

It gives a slice: a pointer into arr’s memory at element 1 and a length of 3, with nothing copied, so writing s[0] = 20 changes arr[1] too. The two share memory until one of them is given new memory. .dup makes a copy at once. An append can do it too: when the slice cannot grow in place, because its capacity is 0 or full, the runtime copies it before appending, and from then on writes through the slice no longer reach arr. In the lesson, s.capacity was 0, since arr still had an element after the slice, so s ~= 40 moved s to new memory and s[0] = 7 left arr as [1, 20, 3, 4, 5]. Because the capacity is not visible in the code, use .dup whenever you mean a copy.

Two longer questions, on an append that sometimes changes the caller’s data and on a release build that reads past an array:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A function takes an int[] batch, appends a marker with ~=, then sets batch[0] to 0 as a flag. Some callers see their first element turn to 0 and others do not, with the same code. Explain why, and how you would rewrite the function.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to staff

    A D parser passes its tests, but in production, built with dub build --build=release, it sometimes returns a wrong field instead of failing on a short line. The debug build always throws ArrayIndexError on the same input. What is going on, and what do you change?

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. After s = arr[1 .. 4] in D, what does s[0] = 20 change?

    arr[1] too: a slice is a pointer into the array's memory plus a length, and nothing is copied. arr[1 .. 4].dup gives a copy with memory of its own.

  2. When does appending to a D slice make it stop sharing memory with its array?

    When it cannot grow in place, for example with capacity 0 because the array goes on after it: the runtime copies the slice before appending. The code does not show this, so take .dup when you mean a copy.

  3. small[5] is read from a three-element D array. What happens, and can catch (Exception e) handle it?

    It stops with ArrayIndexError, naming index 5 and length 3. That is an Error, not an Exception, which the documentation calls unsafe to catch: fix the code instead.

  4. What do length, walkLength and byGrapheme count for the flag 🇮🇳 in D?

    8 UTF-8 code units, 2 code points and 1 character as a reader sees it. A string is an array of code units, so a slice can cut a letter in half.

  5. What are -7 / 2, -7 % 2 and int.max + 1 in D?

    −3, −1 and −2,147,483,648. Division truncates and the remainder keeps the dividend's sign, as in C, and the wrap is defined by the specification; core.checkedint can report it.

  6. writeln prints 0.3 for 0.1 + 0.2, yet the sum compares unequal to 0.3. Why?

    writeln shows a double with six significant digits. Printed with %.17g, the sum is 0.30000000000000004.

  7. What does it mean that iota(1, 11).filter!(…).map!(…) is lazy?

    Nothing is computed until a value is taken. A map over a billion numbers was built at once, and take(2) ran the squaring function exactly twice.

  8. In a --release build, which array reads still have their bounds checked?

    Only reads in @safe functions. The @system read past the end printed 61 and carried on; the @safe one still stopped with ArrayIndexError.

Key takeaways

  • A slice shares its array’s memory, so writes through it change the array; .dup copies, and an append copies only when the slice cannot grow in place, which the code does not show.
  • A string is an array of UTF-8 code units: length counts bytes, walkLength code points and byGrapheme the characters a reader sees, and slicing can cut a character in half.
  • Integer division truncates and overflow wraps, by the specification; round goes half away from zero, lrint to even, and writeln prints a double with six significant digits.
  • Phobos pipelines are lazy ranges chained with UFCS: nothing runs until a value is taken, and take(2) did the work of two values.
  • Structs are copied and classes are shared references; scope guards run in reverse order when a block ends, and an Error such as ArrayIndexError is not caught by catch (Exception e).
  • A release build keeps bounds checks only in @safe functions, so mark indexing code @safe, and run unittest blocks with -unittest or dub test.

Terms in this lesson

attribute inference
The compiler working out attributes such as @safe, pure, nothrow and @nogc from a function's body. D does it for templates, function literals, auto functions and nested functions, and not for other functions.
code unit
The unit in which an encoding stores text: one byte in UTF-8, two in UTF-16. ï takes two UTF-8 code units and 😀 four, and a D string's length counts UTF-8 code units.
Error (D)
A throwable for a failure a program is not expected to recover from, such as ArrayIndexError, RangeError or AssertError. catch (Exception e) does not catch it, and the documentation calls it not safe to catch.
range (D)
An object that hands out values one at a time through empty, front and popFront. Phobos's algorithms take and return ranges, and most of them are lazy: nothing is computed until a value is taken.
scope guard
A statement scope(exit), scope(success) or scope(failure) that runs when the enclosing block ends: always, only without an exception, or only while one leaves. Several run in reverse order, the last written first.
slice (D)
A view of part of an array: a pointer to its first element and a length, written arr[1 .. 4]. Nothing is copied, so writing through the slice changes the array; .dup makes a copy with memory of its own.
UFCS
Uniform Function Call Syntax: in D, a call f(a, b) may be written a.f(b), so that calls chain from left to right, as in iota(1, 11).filter!(x => x % 2 == 0).

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.