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 at compile time: CTFE, templates, mixins

Run ordinary D functions while compiling, constrain templates, put units into types, generate code with mixins and __traits, and decide when -betterC fits.

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

What you will learn

  • Evaluate ordinary functions at compile time with enum and static assert, and predict what that evaluation refuses
  • Write templates with constraints and static if that report a wrong call where it is made
  • Generate code with string mixins and list a struct's fields with __traits and std.traits
  • Decide when -betterC fits a target, and build one source with and without the D runtime

Before you start

What you’ll learn · Before you start

What you will learn

  • Evaluate ordinary functions at compile time with enum and static assert, and predict what that evaluation refuses
  • Write templates with constraints and static if that report a wrong call where it is made
  • Generate code with string mixins and list a struct's fields with __traits and std.traits
  • Decide when -betterC fits a target, and build one source with and without the D runtime

Before you start

On this page

enum fib50 = fib(50); makes the compiler run an ordinary D function while it builds the program, so the program starts with 12,586,269,025 already in it, and static assert(fib(10) == 55) stops the build the day that fact about your code turns false. This is D’s signature feature: the language of the program is also the language that runs while compiling.

Here you evaluate functions at compile time and see what that evaluation refuses, write templates whose constraints report a wrong call where it is made, give quantities their units in their types, generate code with string mixins, inspect a struct’s members with __traits and std.traits, and build one program with and without D’s runtime.

enum and static assert run a function while compiling

fib below is an ordinary function with a loop, and the program uses it twice: once to fill a manifest constant, whose value must exist while compiling, and once with a number known only at run time. A second function builds the table of the CRC-32 that zlib uses, also while compiling. Step through the build and the run:

Interactive · message by message A function that runs while ctfe.d compiles Step through the build and the run of ctfe.d and watch which work happens before the program exists.
A function that runs while ctfe.d compiles, step 7 of 7. Messages so far: Front end to CTFE: fib(50); CTFE to Front end: 12586269025; Front end to The program: fib50, crcTable.Front endreads and checks ctfe.dCTFEruns D sourceThe program./ctfe at run timefib(50)12586269025fib50, crcTable

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

Every step as text
  1. A constant needs a value. The front end reaches enum fib50 = fib(50). A manifest constant must have its value while compiling, so the call cannot wait for the program to run.
  2. Run fib while compiling. The front end hands the call to its interpreter for compile-time function evaluation, which runs fib's own source: the same loop that the program will contain.
  3. The value comes back. The interpreter returns 12586269025, and fib50 is that number from now on. pragma(msg) printed it during the build, with LU, the suffix of a ulong literal.
  4. A fact tested. static assert(fib(10) == 55) is evaluated the same way and is true, so it prints nothing. In wrong.d, which expects 56, the same line stopped the build with an error.
  5. A table built. immutable crcTable = makeCrcTable() runs a loop of 256 by 8 steps while compiling. The 256 numbers go into the program as data; the program never computes them.
  6. What it refuses. The interpreter needs a function's D source and no mutable global state: limits.d failed on getenv, which has no available source code, and on a static counter.
  7. The run. ./ctfe prints the constant 12586269025 and the CRC-32 of hello, 3610a686, which Python's zlib also gave. fib(n) with a run-time n runs the loop again, now as machine code.

ctfe.d: a constant, a checked fact and a table, all made while compiling D · ctfe/ctfe.d
import std.stdio;

/// The n-th Fibonacci number, with a loop: an ordinary function.
ulong fib(uint n)
{
    ulong a = 0, b = 1;
    foreach (i; 0 .. n)
    {
        immutable next = a + b;
        a = b;
        b = next;
    }
    return a;
}

enum fib50 = fib(50);            // a manifest constant: fib runs while compiling
static assert(fib(10) == 55);    // checked while compiling

/// The table of the CRC-32 that zlib and PNG use, one entry per byte value.
uint[256] makeCrcTable()
{
    uint[256] table;
    foreach (uint i; 0 .. 256)
    {
        uint c = i;
        foreach (_; 0 .. 8)
            c = (c & 1) ? 0xEDB88320 ^ (c >> 1) : c >> 1;
        table[i] = c;
    }
    return table;
}

immutable uint[256] crcTable = makeCrcTable();   // computed while compiling, stored in the program

uint crc32(const(ubyte)[] data)
{
    uint c = 0xFFFFFFFF;
    foreach (b; data)
        c = crcTable[(c ^ b) & 0xFF] ^ (c >> 8);
    return c ^ 0xFFFFFFFF;
}

void main()
{
    pragma(msg, "while compiling, fib(50) = ", fib50);
    writeln("fib(50) = ", fib50);
    writefln("crc32(\"hello\") = %08x", crc32(cast(const(ubyte)[]) "hello"));
    uint n = 50;
    writeln("fib(n) at run time = ", fib(n));
}
ctfe.d built and run, a failing static assert and two refusals Python · ctfe_session.py
from session import run

# A function run while compiling: pragma(msg) prints during the build, the program prints at run time, and Python's
# zlib computes the same CRC-32 as the table that was built while compiling. Then a static assert that fails, and two
# functions that compile-time evaluation refuses. -c compiles a module without linking a program.
run("""
cd ctfe
ldc2 ctfe.d
./ctfe
python3 -c "import zlib; print(format(zlib.crc32(b'hello'), '08x'))"
!ldc2 -c wrong.d
!ldc2 -c limits.d
""")

Output

$ cd ctfe
$ ldc2 ctfe.d
while compiling, fib(50) = 12586269025LU
$ ./ctfe
fib(50) = 12586269025
crc32("hello") = 3610a686
fib(n) at run time = 12586269025
$ python3 -c "import zlib; print(format(zlib.crc32(b'hello'), '08x'))"
3610a686
$ ldc2 -c wrong.d
wrong.d(13): Error: static assert:  "fib(10) should be 56"
static assert(fib(10) == 56, "fib(10) should be 56");
^
[exit status 1]
$ ldc2 -c limits.d
limits.d(8): Error: static variable `counter` cannot be read at compile time
    return ++counter;
             ^
limits.d(16):        called from here: `next()`
enum first = next();
                 ^
limits.d(13): Error: `getenv` cannot be interpreted at compile time, because it has no available source code
    return getenv("HOME").fromStringz.idup;
                 ^
[exit status 1]

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

The first output line came from the build, not from the program: pragma(msg) prints while compiling, and it showed 12586269025LU, the constant with the suffix of a ulong literal. This is compile-time function evaluation (CTFE): The D compiler running an ordinary function while it builds the program, wherever a value must exist then: an enum constant, a static initialiser, static assert, static if, a template value argument or a mixin.. The specification lists the places where it happens, among them the initialiser of a manifest constant or of a global or static variable, an argument of a template value parameter, static if, static foreach, static assert, mixins and the size of a static array. The CRC table shows why it matters: 256 entries, worked out bit by bit while compiling, went into the program as data, and its CRC-32 of “hello”, 3610a686, is the value Python’s zlib.crc32 gave in the same session.

The last two commands show what the interpreter refuses. wrong.d claims that fib(10) is 56, and the build stopped with the static assert’s own message. limits.d asks for two constants that cannot exist before the program runs: one reads a mutable counter at module level, a static variable in the compiler’s message, which the specification forbids, and one calls getenv, a C function whose D source the compiler does not have.

Expecting a fast build from a slow function

Compile-time evaluation interprets the function’s source while the compiler runs, and the specification warns that an endless loop may make the compiler hang. Keep compile-time work to tables, constants and checks, and leave heavy computing to the program.

Checkpoint

Check: Running functions while compiling

  1. Question 1 of 2 The session's first output line, "while compiling, fib(50) = 12586269025LU", came from which command?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: ldc2 ctfe.d, the build

    pragma(msg) is evaluated by the compiler, so its text shows during the build. The value, 12586269025, had already been computed by compile-time evaluation of fib(50); LU is the suffix of a ulong literal.

  2. Question 2 of 2 Which of these can an enum initialiser not run while compiling?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • A call of getenv from core.stdc.stdlib
    • A function that increments a mutable static counter

    Ordinary D code runs while compiling, loops and arrays included. What it cannot use is state that exists only at run time: mutable globals or statics, functions without D source such as C calls, and inline assembly.

A constraint reports a wrong call where it is made

A template is code with parameters that the compiler fills in, and it makes one copy of the code per set of arguments. total adds the elements of any array; called with an array of strings, it is a mistake, and the question is where the compiler says so. open.d has no constraint and checked.d has if (isNumeric!T):

checked.d: total with a constraint D · templates/checked.d
import std.traits : isNumeric;

T total(T)(T[] values)
if (isNumeric!T)
{
    T sum = 0;
    foreach (v; values)
        sum += v;
    return sum;
}

void main()
{
    auto a = total([1.5, 2.25]);
    auto b = total(["7", "8"]);
}
A template with and without a constraint, static if, and units in types Python · templates_session.py
from session import run

# One template without a constraint and one with it, called with an array of strings; static if in a template; and a
# units type whose dimension is part of its type, so that adding metres to seconds does not compile.
run("""
cd templates
!ldc2 open.d
!ldc2 checked.d
ldc2 -run describe.d
ldc2 units.d quantity.d
./units
!ldc2 -c units_wrong.d
""")

Output

$ cd templates
$ ldc2 open.d
open.d(3): Error: cannot implicitly convert expression `0` of type `int` to `string`
    T sum = 0;
            ^
open.d(5): Error: slice `sum` is not mutable
        sum += v;
            ^
open.d(5):        did you mean to concatenate (`sum ~= v`) instead ?
        sum += v;
            ^
open.d(12): Error: template instance `open.total!string` error instantiating
    auto b = total(["7", "8"]);
                  ^
[exit status 1]
$ ldc2 checked.d
checked.d(15): Error: template `total` is not callable using argument types `!()(string[])`
    auto b = total(["7", "8"]);
                  ^
checked.d(3):        Candidate is: `total(T)(T[] values)`
  with `T = string`
  must satisfy the following constraint:
`       isNumeric!T`
T total(T)(T[] values)
  ^
[exit status 1]
$ ldc2 -run describe.d
42: a whole number of 4 bytes
42: a whole number of 8 bytes
2.5: a floating-point number
Pune: text of 4 code units
$ ldc2 units.d quantity.d
$ ./units
speed: 10.491 m/s
Quantity!(1, 0): 100.5
Quantity!(1, -2): 1.095
Quantity!(0, 0): 2.01
$ ldc2 -c units_wrong.d
units_wrong.d(5): Error: none of the overloads of template `quantity.Quantity!(1, 0).Quantity.opBinary` are callable using argument types `!("+")(Quantity!(0, 1))`
    auto mixedUp = Metres(100) + Seconds(9.58);
                         ^
quantity.d(9):        Candidates are: `opBinary(string op)(Quantity other) const`
    Quantity opBinary(string op)(Quantity other) const
             ^
quantity.d(16):                        `opBinary(string op, int m2, int s2)(Quantity!(m2, s2) other) const`
  with `op = "+",
       m2 = 0,
       s2 = 1`
  must satisfy one of the following constraints:
`       op == "*"
       op == "/"`
    auto opBinary(string op, int m2, int s2)(Quantity!(m2, s2) other) const
         ^
quantity.d(9):        `opBinary` defined here
    Quantity opBinary(string op)(Quantity other) const
             ^
[exit status 1]

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

Without the constraint, the compiler made total!string and compiled its body, so the errors came from inside it: T sum = 0 could not turn 0 into a string, and sum += v failed too, with a hint to concatenate with ~= instead. Only the last line pointed back to the call, at line 12. With the template constraint: A condition after a template's parameters, such as if (isNumeric!T), checked while compiling. A call whose arguments fail it is refused at the call, with the condition named, before the template's body is compiled., the call itself was refused, at line 15: total is not callable using !()(string[]), with T = string and the constraint that failed. Step through both calls:

Interactive · stage by stage A call of the template total, from the call to the code Step through total([1.5, 2.25]) and total(["7", "8"]) and see where each error is reported.
A call of the template total, from the call to the code, step 6 of 6. Showing The call total(xs), Deduce T (T = string), if (isNumeric!T) (isNumeric!string: false), total!T (total!string), Compile the body (two errors inside). In focus: Compile the body, inside.false: error at the callwithout it: errors insideThe call total(xs)in mainDeduce TT = stringif (isNumeric!T)isNumeric!string: falsetotal!Ttotal!stringCompile the bodytwo errors inside

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

Every step as text
  1. The call. main calls total([1.5, 2.25]). total is a template, so there is no code for it yet: the compiler makes a copy for each type it is called with.
  2. Deduce T. The argument is a double[], so T is double. Nobody wrote total!double; the compiler worked it out from the argument.
  3. Check the constraint. isNumeric!double is true, so the template accepts the call. A constraint is evaluated while compiling, like a static assert.
  4. A copy for double. The compiler instantiates total!double and compiles its body for doubles. A later call with another double[] reuses this copy.
  5. The second call. total(["7", "8"]) deduces T = string. isNumeric!string is false, so no copy is made and the error is reported at the call: total is not callable using argument types !()(string[]), with the constraint named.
  6. Without the constraint. open.d has no constraint, so total!string is made and its body compiled. The errors come from inside it, sum = 0 and sum += v, and only the last line points back to the call.

describe.d uses static if, which chooses code while compiling: in each copy of describe, only the branch for that type remains, so describe(42) says 4 bytes for an int and describe(42L) 8 bytes for a long. The last branch is static assert(false, …), which turns a type that no branch handles into a compile error with a message of your own.

Key idea

Put what a template needs into its constraint: a wrong call is then refused at the call, with the condition that failed, instead of with errors from inside the template’s body.

Checkpoint

Check: Template constraints and static if

  1. Question 1 of 2 checked.d calls total(["7", "8"]) with if (isNumeric!T) on total. Where does the compiler report the error?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: At the call, line 15, naming the constraint isNumeric!T

    A template constraint is checked before the template's body is compiled for the call, so a wrong call gets one error where it is made, with the condition that failed.

  2. Question 2 of 2 describe.d has static if branches for integers, floating-point numbers and strings. What does describe(42L) print?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 42: a whole number of 8 bytes

    static if chooses code while compiling: each copy of describe keeps only the branch for its type, and an unhandled type would hit the static assert at the end.

Types can carry units, checked while compiling

Quantity(int m, int s) in quantity.d keeps a quantity’s dimension in its type: metres to the power m times seconds to the power s. Metres is Quantity!(1, 0), Seconds is Quantity!(0, 1), and their opBinary templates say what each operator gives:

quantity.d: a quantity whose dimension is part of its type D · templates/quantity.d
module quantity;

/// A quantity with its dimension as two exponents: metres^m * seconds^s.
struct Quantity(int m, int s)
{
    double value;

    /// Adding or subtracting needs the same dimension: the parameter's type says so.
    Quantity opBinary(string op)(Quantity other) const
    if (op == "+" || op == "-")
    {
        return Quantity(mixin("value " ~ op ~ " other.value"));
    }

    /// Multiplying adds the exponents, dividing subtracts them.
    auto opBinary(string op, int m2, int s2)(Quantity!(m2, s2) other) const
    if (op == "*" || op == "/")
    {
        static if (op == "*")
            return Quantity!(m + m2, s + s2)(value * other.value);
        else
            return Quantity!(m - m2, s - s2)(value / other.value);
    }
}

alias Metres = Quantity!(1, 0);
alias Seconds = Quantity!(0, 1);
alias MetresPerSecond = Quantity!(1, -1);

static assert(is(typeof(Metres(100) / Seconds(9.58)) == MetresPerSecond));
static assert(!__traits(compiles, Metres(1) + Seconds(1)));

+ and - take a Quantity of the same type, so only equal dimensions add. * and / take any Quantity!(m2, s2) and return a new type with the exponents added or subtracted, which the static if inside chooses. The two static asserts at the end are tests that run while compiling: Metres / Seconds must be MetresPerSecond, and Metres + Seconds must not compile, checked with __traits(compiles, …).

In the session, units.d printed a speed of 10.491 m/s from 100.5 m in 9.58 s, and the types of three expressions as the compiler names them: Quantity!(1, 0) for speed times time, Quantity!(1, -2) for speed per second and Quantity!(0, 0) for a ratio of two lengths. An alias such as Metres is only another name, so typeof shows the template. units_wrong.d, which adds metres to seconds, did not compile: no overload of opBinary accepts Quantity!(0, 1) for +. The program never has to check a unit at run time, because the wrong sum never becomes a program.

Checkpoint

Check: Units carried in types

  1. Question 1 of 2 With Quantity(int m, int s), what is the type of a speed in metres per second divided by a time in seconds?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Quantity!(1, -2)

    opBinary for / returns Quantity!(m - m2, s - s2), so (1, -1) / (0, 1) is (1, -2), metres per second squared.

  2. Question 2 of 2 What happens to Metres(100) + Seconds(9.58), as in units_wrong.d?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It does not compile: no opBinary accepts Quantity!(0, 1) for +

    The parameter type of + is the quantity's own type, so only equal dimensions add. quantity.d also tests this while compiling: static assert(!__traits(compiles, Metres(1) + Seconds(1))).

A string mixin compiles code that a function wrote

A string mixin, mixin(…), compiles a string as D code where it stands. The string is computed while compiling, so a function can write the code. getters below makes one getter per name, and pragma(msg) shows the code it wrote:

getters.d: getters written by a function D · mixins/getters.d
import std.stdio;
import std.format : format;

/// D source for one getter per name, built as a string while compiling.
string getters(string[] names)
{
    string code;
    foreach (n; names)
        code ~= format("int %s() const { return _%s; }\n", n, n);
    return code;
}

struct Score
{
    private int _runs, _wickets;
    this(int runs, int wickets) { _runs = runs; _wickets = wickets; }
    mixin(getters(["runs", "wickets"]));
}

void main()
{
    pragma(msg, getters(["runs", "wickets"]));
    auto s = Score(287, 6);
    writeln(s.runs, "/", s.wickets);
}
tocsv.d: a CSV line for any struct D · mixins/tocsv.d
import std.stdio;
import std.conv : to;
import std.traits : FieldNameTuple;

struct Reading
{
    string station;
    int hour;
    double celsius;
    double fahrenheit() const { return celsius * 9 / 5 + 32; }
}

struct Order
{
    int id;
    string item;
    int quantity;
    bool paid;
}

/// The names of T's fields, in order: the CSV header.
string csvHeader(T)()
{
    string line;
    static foreach (i, name; FieldNameTuple!T)
        line ~= (i ? "," : "") ~ name;
    return line;
}

/// One CSV line with the values of value's fields, in the order of the header.
string toCsv(T)(T value)
{
    string line;
    static foreach (i, name; FieldNameTuple!T)
        line ~= (i ? "," : "") ~ __traits(getMember, value, name).to!string;
    return line;
}

void main()
{
    pragma(msg, "allMembers of Reading: ", __traits(allMembers, Reading));
    pragma(msg, "FieldNameTuple!Reading: ", FieldNameTuple!Reading);
    writeln(csvHeader!Reading);
    writeln(Reading("Pune", 6, 21.5).toCsv);
    writeln(csvHeader!Order);
    writeln(Order(17, "pencils", 12, true).toCsv);
}
getters.d and tocsv.d built and run Python · mixins_session.py
from session import run

# A string mixin whose code is built while compiling, printed by pragma(msg); then a CSV writer for any struct, from
# the names of its fields.
run("""
cd mixins
ldc2 getters.d
./getters
ldc2 tocsv.d
./tocsv
""")

Output

$ cd mixins
$ ldc2 getters.d
int runs() const { return _runs; }
int wickets() const { return _wickets; }
$ ./getters
287/6
$ ldc2 tocsv.d
allMembers of Reading: AliasSeq!("station", "hour", "celsius", "fahrenheit")
FieldNameTuple!Reading: AliasSeq!("station", "hour", "celsius")
$ ./tocsv
station,hour,celsius
Pune,6,21.5
id,item,quantity,paid
17,pencils,12,true

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

The build of getters.d printed the two lines of code that the string mixin: mixin(...) with a string that is known while compiling: the compiler compiles the string as D code in that place, so a function run at compile time can write declarations or statements. compiled into Score, and the program then called them: 287/6. tocsv.d goes the other way: instead of writing code from names, it reads the names from a type. FieldNameTuple!T from std.traits gives the names of T’s fields, and static foreach unrolls one line of code per field, so toCsv works for Reading and for Order without knowing either.

Taking allMembers for the list of fields

__traits(allMembers, Reading) gave station, hour, celsius and fahrenheit: it lists every member, and fahrenheit is a method. A CSV writer built on it would add a column for every method. FieldNameTuple!Reading gave the three fields alone.

Checkpoint

Check: String mixins and a type's fields

  1. Question 1 of 2 When is the string that getters(["runs", "wickets"]) returns turned into code?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: While compiling: mixin compiles the string as D code in that place

    A string mixin needs a string known while compiling, often computed by compile-time evaluation of an ordinary function, and compiles it as declarations or statements in its place.

  2. Question 2 of 2 Reading has the fields station, hour and celsius and the method fahrenheit. What does __traits(allMembers, Reading) list?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: station, hour, celsius and fahrenheit

    A CSV writer built on allMembers would add a column for every method. FieldNameTuple from std.traits gives the fields alone, which is what toCsv uses.

A betterC build keeps the language and drops the runtime

-betterC builds a program that depends on the C library only, without druntime and its garbage collector. The specification gives the reasons: constrained targets, and D code inside a larger C program, which then need not start D’s runtime first. One source can build both ways, because -betterC defines the version D_BetterC:

total.d: one source for both builds D · betterc/total.d
import core.stdc.stdio : printf;

/// Adds the marks without allocating, so it builds with and without -betterC.
int total(const int[] marks) @nogc nothrow
{
    int sum = 0;
    foreach (m; marks)
        sum += m;
    return sum;
}

extern (C) int main()
{
    static immutable int[4] marks = [72, 45, 90, 61];
    version (D_BetterC)
        printf("built with -betterC: ");
    else
        printf("built with the D runtime: ");
    printf("total %d\n", total(marks[]));
    return 0;
}
total.d built twice, and what -betterC refuses Python · betterc_session.py
from session import run

# One source built with the D runtime and with -betterC, and the sizes of both programs; then what -betterC refuses:
# an append, a throw, a class made with new (a link error) and std.stdio's writeln, and what it keeps: a bounds
# check, through C's assert.
run("""
cd betterc
ldc2 total.d -of=total_runtime
./total_runtime
wc -c < total_runtime
ldc2 -betterC total.d -of=total_betterc
./total_betterc
wc -c < total_betterc
!ldc2 -betterC append.d
!ldc2 -betterC throws.d
!ldc2 -betterC newclass.d
# writeln from std.stdio under -betterC: the errors of a build that fails, picked out with grep
ldc2 -betterC phobos.d 2>&1 | grep -e 'Error'
ldc2 -betterC index.d
!./index
""")

Output

$ cd betterc
$ ldc2 total.d -of=total_runtime
$ ./total_runtime
built with the D runtime: total 268
$ wc -c < total_runtime
   50208
$ ldc2 -betterC total.d -of=total_betterc
$ ./total_betterc
built with -betterC: total 268
$ wc -c < total_betterc
   33568
$ ldc2 -betterC append.d
append.d(4): Error: appending to array in `marks ~= 90` requires the GC which is not available with -betterC
    marks ~= 90;
          ^
[exit status 1]
$ ldc2 -betterC throws.d
throws.d(3): Error: cannot use `throw` statements with `-fno-exceptions`
    throw new Exception("no runtime to catch this");
    ^
[exit status 1]
$ ldc2 -betterC newclass.d
Undefined symbols for architecture arm64:
  "__D6object6Object5opCmpMFCQqZi", referenced from:
      __D8newclass7Counter6__vtblZ in newclass.o
  "__D6object6Object6toHashMFNbNeZm", referenced from:
      __D8newclass7Counter6__vtblZ in newclass.o
  "__D6object6Object8opEqualsMFCQtZb", referenced from:
      __D8newclass7Counter6__vtblZ in newclass.o
  "__D6object6Object8toStringMFZAya", referenced from:
      __D8newclass7Counter6__vtblZ in newclass.o
  "__D8newclass7Counter7__ClassZ", referenced from:
      _main in newclass.o
  "__d_allocclass", referenced from:
      _main in newclass.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
Error: /usr/bin/cc failed with status: 1
[exit status 1]
# writeln from std.stdio under -betterC: the errors of a build that fails, picked out with grep
$ ldc2 -betterC phobos.d 2>&1 | grep -e 'Error'
…/bin/../import/std/stdio.d(4281): Error: none of the overloads of template `std.stdio.File.LockingTextWriter.put` are callable using argument types `!()(string)`
…/bin/../import/std/stdio.d(3279): Error: none of the overloads of template `std.utf.decodeFront` are callable using argument types `!()(char[])`
…/bin/../import/std/utf.d(2311): Error: cannot use `throw` statements with `-fno-exceptions`
…/bin/../import/std/utf.d(2514): Error: template instance `std.utf._utfException!(Flag.no)` error instantiating
$ ldc2 -betterC index.d
$ ./index
Assertion failed: (array index out of bounds), function index.main, file index.d, line 7.
[exit status 134]

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

The two builds printed their own line and the same total, 268, and the -betterC program was 33,568 bytes against 50,208. Both use extern (C) int main and printf, so neither pulls in much of the runtime; the hello.d of the first lesson, which used writeln, was 1,188,488. What -betterC refuses shows what the runtime does. An append needs the garbage collector. A throw cannot be compiled, because -betterC switches exceptions off. A class made with new compiled but did not link: Object’s methods and _d_allocclass, the runtime’s allocator, were missing. writeln failed deep inside std.stdio, which needs the runtime too. What stays is most of the language, compile-time evaluation included, and a bounds check: index.d stopped with C’s assertion message and exit status 134.

Interactive · answer and compare D with its runtime, or -betterC? Answer the two questions about where your code will run and what it needs; the table marks what fits and why.
Where will the code run?
Does the code need the GC, classes, exceptions or std.stdio?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

D with its runtime and -betterC against four things a project needs
Build GC, classes, exceptions Phobos, such as writeln Size of a small program Called from C code
D with its runtime Links in: druntime and Phobos Good all of them: appends, new, throw and catch Good all of Phobos, std.stdio included Mixed total.d: 50,208 bytes; with writeln, hello.d was 1,188,488 Mixed the runtime must be started before D code runs
-betterC Links in: the C library only Poor refused: an append, throw, and new on a class Poor only what needs no runtime: writeln did not build Good total.d: 33,568 bytes Good no runtime to start: the spec's reason for it
How the answers decide
  • “Does the code need the GC, classes, exceptions or std.stdio?” Yes, some of them: rules out -betterC. -betterC refuses appends that need the GC, throw statements and classes made with new, and writeln did not build.
  • “Where will the code run?” Inside a C program, or on a target without the runtime: adds a caveat to D with its runtime. A D library with its runtime must have the runtime started before its code runs; -betterC needs only the C library.
  • “Where will the code run?” As a program of its own and “Does the code need the GC, classes, exceptions or std.stdio?” No: structs, static arrays, printf: adds a caveat to -betterC. A program of its own can link the runtime; -betterC saved 16,640 bytes on total.d and costs Phobos and exceptions.

In short: build with the runtime for programs of your own, and with -betterC for D inside a C program or on a target without the runtime, as long as the code can live with structs, static arrays and printf.

Checkpoint

Check: What -betterC keeps and drops

  1. Question 1 of 2 Which of these did the -betterC builds of this lesson refuse?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • A throw statement
    • An append, marks ~= 90

    -betterC keeps most of the language, bounds checks included, and drops what needs druntime: the garbage collector, exceptions, classes made with new, and the parts of Phobos that use them.

  2. Question 2 of 2 How does total.d print a different line in each build from one source?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: version (D_BetterC) chooses the line while compiling

    The specification: when -betterC is on, the predefined version D_BetterC can be used for conditional compilation, so one source can build both ways.

Practise: the units, in Python

The exercise writes the rules of Quantity as Python functions on pairs of exponents: what the compiler works out while compiling, done at run time. Its sample tests replay the four lines of units.d and the sum that units_wrong.d could not compile.

Exercise · Medium · Python

Work out the type D gives each unit expression

The Quantity(int m, int s) template of this lesson keeps a quantity's dimension in its type: metres to the power m times seconds to the power s. The compiler works the type of every expression out while compiling. Write the same rules in units.py, with a dimension as a pair (m, s) of whole numbers:

  • times(a, b): the dimension of a product, the exponents added: times((1, -1), (0, 1)) is (1, 0).
  • divided(a, b): the dimension of a quotient, the exponents subtracted: divided((1, 0), (0, 1)) is (1, -1).
  • plus(a, b): the dimension of a sum, which exists only when a and b are the same; otherwise raise TypeError with the message cannot add Quantity!(1, 0) and Quantity!(0, 1), naming the two types as below.
  • type_name(d): the name D prints for typeof(…).stringof, such as Quantity!(1, -2). An alias such as Metres is only another name, so typeof shows the template, as units.d printed Quantity!(1, 0) for speed * time.

The sample tests replay the four lines that units.d printed and the sum that units_wrong.d could not compile.

Starter code · units.py

NAMES = {(1, 0): "Metres", (0, 1): "Seconds", (1, -1): "MetresPerSecond"}


def times(a, b):
    """The dimension of a * b."""
    return (a[0] * b[0], a[1] * b[1])


def divided(a, b):
    """The dimension of a / b."""
    return (a[0] - b[0], a[1] - b[1])


def plus(a, b):
    """The dimension of a + b, or a TypeError when the dimensions differ."""
    return a


def type_name(d):
    """What D prints for the type of a quantity with dimension d."""
    return NAMES.get(d, f"Quantity!{d}")
The sample tests · test_units.py
from units import divided, plus, times, type_name

METRES, SECONDS = (1, 0), (0, 1)


def test_speed_from_distance_and_time():
    """distance / time is MetresPerSecond, Quantity!(1, -1)"""
    speed = divided(METRES, SECONDS)
    assert speed == (1, -1)
    assert type_name(speed) == "Quantity!(1, -1)"


def test_back_to_metres():
    """units.d printed Quantity!(1, 0) for speed * time"""
    assert type_name(times(divided(METRES, SECONDS), SECONDS)) == "Quantity!(1, 0)"


def test_speed_per_second():
    """units.d printed Quantity!(1, -2) for speed / time"""
    assert type_name(divided(divided(METRES, SECONDS), SECONDS)) == "Quantity!(1, -2)"


def test_a_ratio_has_no_dimension():
    """units.d printed Quantity!(0, 0) for distance / Metres(50)"""
    assert type_name(divided(METRES, METRES)) == "Quantity!(0, 0)"


def test_metres_plus_metres():
    """the sum in units.d: Metres(100) + Metres(0.5)"""
    assert plus(METRES, METRES) == METRES


def test_metres_plus_seconds_does_not_exist():
    """units_wrong.d did not compile; the model raises instead"""
    try:
        plus(METRES, SECONDS)
    except TypeError as e:
        assert str(e) == "cannot add Quantity!(1, 0) and Quantity!(0, 1)"
    else:
        assert False, "plus((1, 0), (0, 1)) must raise TypeError"


def test_areas_and_more():
    """products add exponents: an area, and an area per second squared"""
    area = times(METRES, METRES)
    assert area == (2, 0)
    assert type_name(divided(area, times(SECONDS, SECONDS))) == "Quantity!(2, -2)"
A hint

Unpack each pair, m1, s1 = a, and return a new tuple. For type_name, write the two numbers into the text with an f-string; Python prints negative numbers with their minus sign, as D does.

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.

Then the self-check, on the five ideas of this lesson. It keeps to the idea you are least sure of until your answers show that you have it, and explains each answer as you give it:

Practice that adapts to you

Practise this lesson

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

  1. Question 1 of 10 In which of these places does D evaluate a function call while compiling?

    Choose every answer that is right.

    Show the answer: Question 1 of 10

    Answer:

    • static assert(f() == 1);
    • enum x = f();

    Among the places the specification lists are initialisers of manifest constants and of global or static variables, static if, static foreach, static assert, template value arguments, mixins and static array sizes.

  2. Question 2 of 10 How did the session show that the CRC table built while compiling is right?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: The program's crc32 of "hello", 3610a686, matched Python's zlib.crc32 of the same bytes

    Computing a table while compiling costs nothing at run time, but it does not make the table correct. A comparison with a trusted implementation, or a static assert on known entries, checks it.

  3. Question 3 of 10 You publish a library template sort2(T) that needs T to support <. A user calls it with a struct without opCmp. What does a constraint such as if (is(typeof(T.init < T.init))) give them?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: One error at their call, naming the condition their type fails

    Constraints are the documentation the compiler enforces: they move the error from the library's internals to the user's call, with the condition in words.

  4. Question 4 of 10 Why does describe end with static assert(false, ...) in its last else branch?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: So that a type no branch handles fails to compile, with a message of its own

    static if keeps only one branch per type, so static assert(false) in the last branch is a compile error that only the unhandled types meet.

  5. Question 5 of 10 What is the metres exponent m of the type of Metres(2) * Metres(3)?

    Type a number.

    Show the answer: Question 5 of 10

    Answer: 2

    Multiplying adds the exponents: (1, 0) and (1, 0) give Quantity!(2, 0), an area in square metres, of value 6.

  6. Question 6 of 10 speed * time has the dimension of Metres. Why did units.d print Quantity!(1, 0) instead of Metres?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: Metres is an alias, only another name, and typeof names the template instance

    An alias declares a name for an existing type; it does not make a new type, so typeof(...).stringof shows the template instance it stands for.

  7. Question 7 of 10 You need one getter per name in a list known while compiling. Why a string mixin rather than an ordinary template function?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: A string mixin can declare new members whose names come from the list

    Templates are parameterised code with fixed names; a string mixin compiles code that a function wrote, so the names themselves can come from data.

  8. Question 8 of 10 What header did csvHeader!Order print?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: id,item,quantity,paid

    The header and the values come from the same list of fields, so they always match: 17,pencils,12,true under id,item,quantity,paid.

  9. Question 9 of 10 total.d was 50,208 bytes with the D runtime and 33,568 bytes with -betterC. How many bytes did -betterC save?

    Type a number.

    A hint

    Subtract the two numbers that wc -c printed.

    Show the answer: Question 9 of 10

    Answer: 16640 bytes

    50,208 − 33,568 = 16,640 bytes, about a third. Both builds already avoided writeln and the garbage collector; the hello.d of the first lesson, with writeln, was 1,188,488 bytes.

  10. Question 10 of 10 Which project fits -betterC best?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: A checksum routine compiled into an existing C program, using static arrays and printf

    -betterC suits D inside C programs and on targets without the runtime, as long as the code lives without the GC, exceptions, classes made with new and most of Phobos.

Interview questions

Warm-up (mid-level to senior): What is compile-time function evaluation in D, and where does it run?

It is the compiler running ordinary D functions while it builds the program, through an interpreter in its front end. It runs wherever a value must exist while compiling: the initialiser of an enum manifest constant or of a global or static variable, static assert, static if, static foreach, a template value argument, a string mixin and the size of a static array. The function needs no special form, but it must have D source available and must not touch mutable global or static state, inline assembly or undefined behaviour. In the lesson, enum fib50 = fib(50) put 12,586,269,025 into the program, a CRC-32 table was built while compiling and matched zlib’s result, and calls of getenv and of a function that changes a static counter were refused.

Two longer questions, on the errors a library’s templates give its users and on D inside a C program:

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

  1. Mid to staff

    Users of your D library's generic function report pages of errors from inside your code whenever they pass a type it cannot handle. How do you change the function so that a wrong call fails clearly, and how do you keep it that way?

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

  2. Senior to staff

    A team wants to write new modules of a large C program in D, one at a time, without changing how the program is built or started. Would you use -betterC, and what would you tell the team about what they give up and how to check the result?

    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. What does enum fib50 = fib(50); make the D compiler do?

    Run the ordinary function fib during the build, so the program contains 12,586,269,025 from the start. This is compile-time function evaluation.

  2. Name four places where D runs a function while compiling.

    The initialiser of an enum constant or of a global or static variable, static assert, static if and static foreach; template value arguments, mixins and static array sizes too.

  3. Which functions can D's compile-time interpreter not run?

    Functions whose D source it lacks, such as the C function getenv, code that reads or writes mutable global or static state, and inline assembly. It interprets, so heavy work stays at run time.

  4. When does pragma(msg) print its text?

    During the build: compiling ctfe.d printed 12586269025LU, the constant with a ulong suffix. The program itself prints nothing for it.

  5. What does adding if (isNumeric!T) to total change for a call with an array of strings?

    The call itself is refused, at its own line, naming the constraint that failed. Without it, the compiler builds total!string and reports errors from inside its body.

  6. How does Quantity!(m, s) keep metres from being added to seconds?

    Its + accepts only a Quantity of the same type, so Metres + Seconds matches no opBinary and does not compile. * and / return a type whose exponents are added or subtracted.

  7. Why is __traits(allMembers, Reading) the wrong list for writing CSV columns?

    It lists every member, methods such as fahrenheit included. FieldNameTuple!Reading from std.traits lists the fields alone.

  8. What does a -betterC build give up, and what does it keep?

    It drops druntime: the garbage collector and with it appends, exceptions, classes made with new, and most of Phobos, writeln included. Most of the language stays, compile-time evaluation and bounds checks too.

Key takeaways

  • enum, static assert, static initialisers and the other places of the specification run ordinary D functions while compiling; pragma(msg) showed the value during the build, and a CRC table built that way matched zlib.
  • The interpreter refuses functions without D source, mutable global or static state and inline assembly, and it interprets, so heavy work belongs at run time.
  • A template constraint refuses a wrong call at the call, naming the condition; without it the errors come from inside the template’s body. static if keeps only the branch for each type.
  • A type such as Quantity!(m, s) carries its units, so Metres + Seconds does not compile, and static assert with __traits(compiles, …) tests that it does not.
  • A string mixin compiles code that a function wrote, and FieldNameTuple lists a struct’s fields, while __traits(allMembers, …) also lists its methods.
  • -betterC keeps the language and needs only the C library, at the cost of the GC, exceptions, classes made with new and most of Phobos; version (D_BetterC) builds one source both ways.

Terms in this lesson

compile-time function evaluation (CTFE)
The D compiler running an ordinary function while it builds the program, wherever a value must exist then: an enum constant, a static initialiser, static assert, static if, a template value argument or a mixin.
string mixin
mixin(...) with a string that is known while compiling: the compiler compiles the string as D code in that place, so a function run at compile time can write declarations or statements.
template constraint
A condition after a template's parameters, such as if (isNumeric!T), checked while compiling. A call whose arguments fail it is refused at the call, with the condition named, before the template's body is compiled.

References

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.