Hello World & Modules
Hello, World
D uses
import modules instead of #include headers — the compiler reads the imported module directly (its .d source or a .di interface file), so there are no header guards and no textual pasting.#include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
}import std.stdio;
void main() {
writeln("Hello, World!");
}writeln appends a newline automatically. The main function returns void by default and implicitly exits with code 0.Selective imports
D allows selective imports —
import module : name brings only the named symbols into scope.#include <stdio.h>
#include <math.h>
int main(void) {
printf("sqrt(2) = %.4f\n", sqrt(2.0));
printf("PI = %.6f\n", M_PI);
return 0;
}import std.stdio : writefln;
import std.math : sqrt, PI;
void main() {
writefln("sqrt(2) = %.4f", sqrt(2.0));
writefln("PI = %.6f", PI);
}This is idiomatic D style — it documents intent and prevents namespace pollution without requiring name prefixes. Wildcard
import std.stdio; also works when you want everything.Compile & run
D ships with three compilers: DMD (reference, fastest compile times), LDC (LLVM-based, best runtime performance), and GDC (GCC backend).
// gcc -std=c17 -Wall -o hello hello.c && ./hello
// With math: gcc hello.c -lm -o hello
// With sanitizers: gcc -fsanitize=address,undefined -o hello hello.c// dmd hello.d && ./hello
// With tests: dmd -unittest -run hello.d
// Optimized: dmd -O -release hello.d
// Using LDC (LLVM): ldc2 -O2 hello.dThe
-unittest flag compiles and runs all unittest blocks before main. The -release flag disables contracts and asserts for production builds.Formatted output
D's
writefln uses the same %d/%s/%f format verbs as C's printf, so the transition is friction-free.#include <stdio.h>
int main(void) {
const char *name = "Alice";
int age = 30;
double score = 98.5;
printf("%-10s age %3d score %6.2f\n", name, age, score);
return 0;
}import std.stdio : writefln;
void main() {
string name = "Alice";
int age = 30;
double score = 98.5;
writefln("%-10s age %3d score %6.2f", name, age, score);
}Unlike
printf, the arguments are type-checked — a mismatched verb throws instead of corrupting output. The std.format module also provides a format(...) function that returns the formatted string.Variables & Types
Fixed-size integer types
D's integer types have fixed sizes on every platform:
int is always 32-bit, long always 64-bit, byte/ubyte always 8-bit.#include <stdio.h>
#include <stdint.h>
int main(void) {
// On 64-bit Linux: int=32, long=64, on MSVC: long=32
int32_t small = 2147483647;
int64_t large = 9223372036854775807LL;
uint8_t byte_val = 255;
printf("%d %lld %u\n", small, large, byte_val);
return 0;
}import std.stdio : writeln;
void main() {
// D integer types are always the same size on every platform
int small = 2147483647; // always 32-bit
long large = 9223372036854775807L; // always 64-bit
ubyte byte_val = 255; // always 8-bit unsigned
writeln(small, " ", large, " ", byte_val);
}C's
long is 32-bit on 64-bit Windows and 64-bit on 64-bit Linux, which is why stdint.h exists. D eliminates this portability hazard at the type level.Type inference with auto
auto is true type inference — the compiler deduces the exact type from the initializer, and the inferred type is fixed at compile time.#include <stdio.h>
int main(void) {
// C requires explicit types everywhere
int count = 42;
double ratio = 3.14;
const char *greeting = "hello";
printf("%d %.2f %s\n", count, ratio, greeting);
return 0;
}import std.stdio : writeln;
void main() {
auto count = 42; // inferred: int
auto ratio = 3.14; // inferred: double
auto greeting = "hello"; // inferred: string
writeln(count, " ", ratio, " ", greeting);
}auto is not a dynamic type; auto greeting = "hello" is exactly a string variable. It works for any variable, including loop variables.immutable vs const
D distinguishes
const — a read-only view, where the underlying data may still change via another reference, exactly C's const semantics — from immutable, a transitive guarantee that no one can change the data at all.#include <stdio.h>
void print_value(const int *ptr) {
// const in C: this pointer can't modify the int
// but another non-const pointer to the same int could
printf("%d\n", *ptr);
}
int main(void) {
int mutable_val = 42;
const int fixed = 100;
print_value(&mutable_val); // still modifiable via mutable_val
printf("%d\n", fixed);
return 0;
}import std.stdio : writeln;
void printValue(const(int)* ptr) {
// const in D: read-only view — someone else might modify it
writeln(*ptr);
}
void main() {
int mutableVal = 42;
const int fixed = 100; // read-only view of this variable
immutable int permanent = 99; // no pointer anywhere can modify this
printValue(&mutableVal);
writeln(fixed, " ", permanent);
}D strings are
immutable(char)[], which is why they can be safely shared across threads without locks.Type aliases with alias
alias replaces C's typedef: a transparent name for an existing type, just as typedef is.#include <stdio.h>
#include <stdint.h>
typedef uint32_t NodeId;
typedef struct { float x; float y; } Point2D;
int main(void) {
NodeId node = 42;
Point2D origin = {0.0f, 0.0f};
printf("node=%u origin=(%.1f,%.1f)\n", node, origin.x, origin.y);
return 0;
}import std.stdio : writeln;
alias NodeId = uint;
struct Point2D { float x; float y; }
alias Origin = Point2D;
void main() {
NodeId node = 42;
Origin origin = Origin(0.0f, 0.0f);
writeln("node=", node, " origin=(", origin.x, ",", origin.y, ")");
}It is more powerful than
typedef, though — alias can also name templates, functions, overload sets, and even individual members, not just types.typeof and type properties
Every D type exposes compile-time properties via dot syntax —
int.sizeof, int.min, int.max — replacing C's sizeof operator and limits.h macros.#include <stdio.h>
#include <stdint.h>
#include <limits.h>
int main(void) {
int value = 42;
printf("size=%zu min=%d max=%d\n",
sizeof(int), INT_MIN, INT_MAX);
return 0;
}import std.stdio : writeln;
void main() {
int value = 42;
// Type properties are built into every type
writeln("size=", int.sizeof,
" min=", int.min,
" max=", int.max);
// typeof infers the type of an expression
typeof(value) other = 100;
writeln(other);
}Other useful properties include
.init (the default value) and .stringof (the type name as a string). typeof(expr) produces the type of an expression at compile time, analogous to C++'s decltype.Arrays & Slices
Static arrays
D puts the array size in the type, not after the variable name:
int[5] numbers rather than int numbers[5].#include <stdio.h>
int main(void) {
int numbers[5] = {10, 20, 30, 40, 50};
printf("len=%zu first=%d last=%d\n",
sizeof(numbers)/sizeof(numbers[0]),
numbers[0], numbers[4]);
return 0;
}import std.stdio : writeln;
void main() {
int[5] numbers = [10, 20, 30, 40, 50];
writeln("len=", numbers.length,
" first=", numbers[0],
" last=", numbers[4]);
}The
.length property replaces the sizeof/sizeof division. Static arrays in D are value types — assigning one copies all elements.Dynamic arrays
D dynamic arrays (
int[]) are GC-managed — no malloc/free, no capacity bookkeeping — and ~= appends.#include <stdio.h>
#include <stdlib.h>
int main(void) {
int capacity = 4;
int *numbers = malloc(capacity * sizeof(int));
int length = 0;
numbers[length++] = 10;
numbers[length++] = 20;
numbers[length++] = 30;
for (int i = 0; i < length; i++) printf("%d ", numbers[i]);
printf("\n");
free(numbers);
return 0;
}import std.stdio : writeln;
void main() {
int[] numbers; // empty dynamic array
numbers ~= 10; // append with ~=
numbers ~= 20;
numbers ~= 30;
writeln(numbers); // [10, 20, 30]
writeln(numbers.length);
}Internally a dynamic array is a fat pointer: a length plus a pointer to GC-managed memory. Passing one to a function passes both the length and pointer by value.
Array slices
D's
a[i..j] slice syntax creates a view into the original array — it shares memory, so modifications to the slice affect the original.#include <stdio.h>
int main(void) {
int numbers[] = {10, 20, 30, 40, 50};
// Slice is just a pointer + length — manual bookkeeping
int *middle = &numbers[1];
int middle_len = 3;
for (int i = 0; i < middle_len; i++) printf("%d ", middle[i]);
printf("\n");
return 0;
}import std.stdio : writeln;
void main() {
int[] numbers = [10, 20, 30, 40, 50];
int[] middle = numbers[1..4]; // elements at index 1, 2, 3
writeln(middle); // [20, 30, 40]
writeln(numbers[2..$]); // from index 2 to end: [30, 40, 50]
}The special
$ symbol means "the length of the array being sliced." Slices are the primary reason D rarely needs pointer arithmetic.Concatenation and copy
The
~ operator concatenates arrays into a new array, and .dup makes an independent copy — replacing C's malloc + double memcpy.#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
int first[] = {1, 2, 3};
int second[] = {4, 5, 6};
int len = 6;
int *combined = malloc(len * sizeof(int));
memcpy(combined, first, 3 * sizeof(int));
memcpy(combined + 3, second, 3 * sizeof(int));
for (int i = 0; i < len; i++) printf("%d ", combined[i]);
printf("\n");
free(combined);
return 0;
}import std.stdio : writeln;
void main() {
int[] first = [1, 2, 3];
int[] second = [4, 5, 6];
int[] combined = first ~ second; // concatenate with ~
writeln(combined); // [1, 2, 3, 4, 5, 6]
int[] copy = first.dup; // deep copy
copy[0] = 99;
writeln(first[0]); // 1 — original unchanged
}.idup makes an immutable copy. Since slices share memory, .dup is needed whenever you want an independent copy to modify.Iterating with index
D's
foreach (index, element; collection) form provides both index and value without manual bookkeeping.#include <stdio.h>
int main(void) {
const char *colors[] = {"red", "green", "blue"};
int len = 3;
for (int i = 0; i < len; i++) {
printf("%d: %s\n", i, colors[i]);
}
return 0;
}import std.stdio : writefln;
void main() {
string[] colors = ["red", "green", "blue"];
foreach (i, color; colors) {
writefln("%d: %s", i, color);
}
}The variable before the semicolon is the index; the one after is the element. Both are typed by inference. Omitting the index (
foreach (color; colors)) iterates values only.Strings
String type
D's
string type is immutable(char)[] — a slice of immutable UTF-8 bytes with no null terminator.#include <stdio.h>
#include <string.h>
int main(void) {
const char *greeting = "Hello, World!";
printf("len=%zu\n", strlen(greeting));
printf("first char: %c\n", greeting[0]);
// No bounds checking — writing past the end is UB
return 0;
}import std.stdio : writeln;
void main() {
string greeting = "Hello, World!";
writeln("len=", greeting.length);
writeln("first char: ", greeting[0]);
// greeting[0] = 'h'; // compile error: string is immutable
}.length is always accurate, and indexing gives a char (a UTF-8 byte). To iterate over Unicode code points, use foreach (dchar c; str) or import std.utf : byDchar.String operations
String concatenation with
~ returns a new string without mutating either operand — no buffer sizing, no strcpy/strcat pair.#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(void) {
const char *first = "Hello";
const char *second = " World";
char *combined = malloc(strlen(first) + strlen(second) + 1);
strcpy(combined, first);
strcat(combined, second);
printf("%s (len=%zu)\n", combined, strlen(combined));
free(combined);
return 0;
}import std.stdio : writeln;
import std.string : toUpper, strip;
void main() {
string first = "Hello";
string second = " World";
string combined = first ~ second; // concatenate
writeln(combined, " (len=", combined.length, ")");
writeln(combined.toUpper());
writeln(" padded ".strip());
}The
std.string module provides toUpper, toLower, strip, split, indexOf, and dozens of other operations. All of them work on string, char[], and wstring uniformly.String conversion
std.conv.to is one generic function for every conversion: strings to numbers, numbers to strings, enums to strings, and more. The exclamation mark (to!int) is D's template instantiation syntax.#include <stdio.h>
#include <stdlib.h>
int main(void) {
// String to number
int parsed = atoi("42");
double parsed_d = atof("3.14");
// Number to string
char buffer[32];
snprintf(buffer, sizeof(buffer), "%d", parsed);
printf("%d %.2f '%s'\n", parsed, parsed_d, buffer);
return 0;
}import std.stdio : writeln;
import std.conv : to;
void main() {
// String to number
int parsed = to!int("42");
double parsed_d = to!double("3.14");
// Number to string
string text = to!string(parsed);
writeln(parsed, " ", parsed_d, " '", text, "'");
// Shorthand
writeln(42.to!string); // UFCS style
}It throws
ConvException on failure rather than silently returning 0 like atoi.Building strings with format
std.format.format is the string-building equivalent of snprintf — it returns a string rather than writing into a caller-supplied buffer.#include <stdio.h>
#include <string.h>
int main(void) {
char buffer[256];
int x = 10, y = 20;
snprintf(buffer, sizeof(buffer), "Point(%d, %d)", x, y);
printf("%s\n", buffer);
return 0;
}import std.stdio : writeln;
import std.format : format;
void main() {
int x = 10, y = 20;
string description = format("Point(%d, %d)", x, y);
writeln(description);
// String interpolation via ~ and to!string
import std.conv : to;
string msg = "x=" ~ x.to!string ~ " y=" ~ y.to!string;
writeln(msg);
}There is no size limit and no buffer overflow risk. For simple cases,
~ concatenation with to!string conversions also works.Control Flow
foreach over integer ranges
foreach (i; 0..5) iterates 0 through 4 — the 0..5 integer range literal replaces C's three-clause for header, and foreach_reverse counts down.#include <stdio.h>
int main(void) {
for (int i = 0; i < 5; i++) {
printf("%d ", i);
}
printf("\n");
// Reverse: explicit bookkeeping
for (int i = 4; i >= 0; i--) {
printf("%d ", i);
}
printf("\n");
return 0;
}import std.stdio : writeln, write;
void main() {
foreach (i; 0..5) {
write(i, " ");
}
writeln();
// Reverse with foreach_reverse
foreach_reverse (i; 0..5) {
write(i, " ");
}
writeln();
}The end of the range is exclusive, matching C's
i < 5 convention. For strided iteration, std.range.iota(start, stop, step) provides stepped ranges.Switch without fallthrough
D's
switch makes accidental fallthrough a compile error — every case needs an explicit break or goto case.#include <stdio.h>
int main(void) {
int status = 2;
switch (status) {
case 1: printf("one\n"); break; // must break!
case 2: printf("two\n"); break;
case 3:
case 4: printf("three or four\n"); break;
default: printf("other\n");
}
return 0;
}import std.stdio : writeln;
void main() {
int status = 2;
switch (status) {
case 1: writeln("one"); break;
case 2: writeln("two"); break;
case 3, 4: writeln("three or four"); break;
default: writeln("other");
}
// Intentional fallthrough requires: goto case;
}Multiple values can be listed as
case 3, 4: instead of stacking empty cases. The goto case statement falls through to the next case when you genuinely want that behavior.Labeled break for nested loops
D supports labeled
break and continue — place a label before the outer loop and break label exits it directly, replacing C's flag-variable or goto escape patterns.#include <stdio.h>
#include <stdbool.h>
int main(void) {
bool found = false;
int found_i = -1, found_j = -1;
for (int i = 0; i < 4 && !found; i++) {
for (int j = 0; j < 4 && !found; j++) {
if (i * 4 + j == 10) {
found = true; found_i = i; found_j = j;
}
}
}
if (found) printf("found at (%d,%d)\n", found_i, found_j);
return 0;
}import std.stdio : writefln;
void main() {
outer: foreach (i; 0..4) {
foreach (j; 0..4) {
if (i * 4 + j == 10) {
writefln("found at (%d,%d)", i, j);
break outer; // break out of the labeled loop
}
}
}
}continue outer similarly skips to the next iteration of the labeled outer loop.static if for compile-time branching
static if is a compile-time conditional that operates on real D expressions, not preprocessor text — only the selected branch is compiled.#include <stdio.h>
// C uses the preprocessor — text substitution, not real branching
#define DEBUG 1
int main(void) {
#if DEBUG
printf("Debug mode\n");
#else
printf("Release mode\n");
#endif
printf("Size of pointer: %zu\n", sizeof(void *));
return 0;
}import std.stdio : writeln;
enum bool debugMode = true;
void main() {
static if (debugMode) {
writeln("Debug mode");
} else {
writeln("Release mode");
}
// static if works inside functions too
static if (size_t.sizeof == 8) {
writeln("64-bit platform");
}
}Unlike
#if, static if can appear inside functions, structs, and templates, and can inspect type properties and template parameters.Functions
Default parameter values
D supports default parameter values directly in the function signature — no duplicate wrapper function needed.
#include <stdio.h>
#include <string.h>
// C has no default params — overload by naming or sentinel values
void greet_full(const char *name, const char *title) {
printf("Hello, %s %s!\n", title, name);
}
void greet(const char *name) { greet_full(name, ""); }
int main(void) {
greet("Alice");
greet_full("Bob", "Dr.");
return 0;
}import std.stdio : writefln;
void greet(string name, string title = "") {
if (title.length > 0)
writefln("Hello, %s %s!", title, name);
else
writefln("Hello, %s!", name);
}
void main() {
greet("Alice"); // uses default title=""
greet("Bob", "Dr.");
}Parameters with defaults must come after parameters without them, and default values must be compile-time expressions.
Function overloading
D supports full function overloading — multiple functions with the same name, differentiated by parameter types — where C needs distinct names or
_Generic.#include <stdio.h>
// C has no overloading — must use different names or _Generic
void print_int(int x) { printf("int: %d\n", x); }
void print_double(double x) { printf("double: %.2f\n", x); }
void print_str(const char *x) { printf("string: %s\n", x); }
int main(void) {
print_int(42);
print_double(3.14);
print_str("hello");
return 0;
}import std.stdio : writefln;
void display(int x) { writefln("int: %d", x); }
void display(double x) { writefln("double: %.2f", x); }
void display(string x) { writefln("string: %s", x); }
void main() {
display(42);
display(3.14);
display("hello");
}The compiler selects the best match at the call site. Overloading works across modules and interacts correctly with templates and UFCS, so
42.display() also resolves to the int overload.Universal Function Call Syntax
Universal Function Call Syntax (UFCS): any free function
f(x, args) can be called as x.f(args).#include <stdio.h>
#include <string.h>
#include <ctype.h>
int word_count(const char *s) {
int count = 0, in_word = 0;
while (*s) {
if (isspace(*s)) in_word = 0;
else if (!in_word) { in_word = 1; count++; }
s++;
}
return count;
}
int main(void) {
// Free functions — method syntax not available
printf("%d words\n", word_count("hello world foo"));
return 0;
}import std.stdio : writeln;
import std.uni : isWhite;
import std.conv : to;
int wordCount(string text) {
bool inWord = false;
int words = 0;
foreach (ch; text) {
if (ch.isWhite) inWord = false;
else if (!inWord) { inWord = true; words++; }
}
return words;
}
void main() {
// UFCS: free function called as method on its first argument
writeln("hello world foo".wordCount(), " words");
writeln(42.to!string); // UFCS: to is a free function
writeln(3.14.to!string);
}This enables method chaining on any type without modifying it — the foundation of D's range pipeline style (
data.filter!(...).map!(...).sum()). There is no performance difference; the compiler transforms one form to the other.Nested functions
D has fully portable nested functions — a core language feature, not the non-standard GCC extension C programmers may know.
#include <stdio.h>
// C has no nested functions (GCC extension only, non-portable)
static int helper(int x) { return x * x; }
int compute(int base) {
return helper(base) + helper(base + 1);
}
int main(void) {
printf("%d\n", compute(3));
return 0;
}import std.stdio : writeln;
int compute(int base) {
// Nested function — only visible inside compute()
int helper(int x) { return x * x; }
return helper(base) + helper(base + 1);
}
void main() {
writeln(compute(3)); // 9 + 16 = 25
}Nested functions are invisible outside their enclosing scope and can access and modify the enclosing function's variables (becoming closures when those variables must outlive the call).
Function attributes
D function attributes are verified by the compiler, not just hints like C's
__attribute__ extensions.#include <stdio.h>
// C uses non-standard __attribute__ extensions
static __attribute__((pure)) int square(int x) { return x * x; }
int main(void) {
printf("%d\n", square(7));
return 0;
}import std.stdio : writeln;
// D function attributes are first-class and checked by the compiler
pure int square(int x) { return x * x; }
@safe pure nothrow int clamp(int value, int lo, int hi) {
if (value < lo) return lo;
if (value > hi) return hi;
return value;
}
void main() {
writeln(square(7));
writeln(clamp(15, 0, 10));
}pure means no global state access (enforced); @safe means no pointer casts and no undefined behavior; nothrow means no exceptions; @nogc means no GC allocations. They compose — a @safe pure nothrow function is maximally constrained and highly optimizable.Structs
Structs with methods
D structs are value types like C structs, but they can carry methods, constructors, and operator overloads.
#include <stdio.h>
#include <math.h>
typedef struct {
double x, y;
} Point;
// Methods must be free functions in C
double point_distance(const Point *p) {
return sqrt(p->x * p->x + p->y * p->y);
}
int main(void) {
Point origin = {3.0, 4.0};
printf("%.1f\n", point_distance(&origin));
return 0;
}import std.stdio : writeln;
import std.math : sqrt;
struct Point {
double x, y;
double distance() const {
return sqrt(x * x + y * y);
}
Point translate(double dx, double dy) const {
return Point(x + dx, y + dy);
}
}
void main() {
Point origin = Point(3.0, 4.0);
writeln(origin.distance()); // 5
writeln(origin.translate(1, 0)); // Point(4, 4)
}The
const method qualifier means the method does not modify the struct — the compiler enforces this. Structs are stack-allocated by default; use new to heap-allocate a pointer to one.Struct constructors and invariants
D structs use
this(params) as the constructor, called with the struct name as a function: Rectangle(5, 3).#include <stdio.h>
#include <assert.h>
typedef struct {
int width, height;
} Rectangle;
Rectangle rectangle_new(int width, int height) {
assert(width > 0 && height > 0);
Rectangle r = {width, height};
return r;
}
int main(void) {
Rectangle rect = rectangle_new(5, 3);
printf("%d x %d\n", rect.width, rect.height);
return 0;
}import std.stdio : writeln;
struct Rectangle {
int width, height;
this(int width, int height)
in (width > 0 && height > 0, "dimensions must be positive")
{
this.width = width;
this.height = height;
}
int area() const { return width * height; }
}
void main() {
Rectangle rect = Rectangle(5, 3);
writeln(rect.width, " x ", rect.height, " area=", rect.area());
}The inline contract
in (condition, "message") on the constructor is checked at runtime in debug builds and skipped in release builds (-release).Operator overloading
D overloads operators through named template methods:
opBinary(string op : "+") handles +, opEquals handles ==.#include <stdio.h>
#include <stdbool.h>
typedef struct { int x, y; } Vector2;
// Must use named functions — no operator syntax
bool vector2_equals(Vector2 a, Vector2 b) { return a.x == b.x && a.y == b.y; }
Vector2 vector2_add(Vector2 a, Vector2 b) { return (Vector2){a.x+b.x, a.y+b.y}; }
int main(void) {
Vector2 v1 = {1, 2}, v2 = {3, 4};
Vector2 v3 = vector2_add(v1, v2);
printf("(%d,%d) equal=%d\n", v3.x, v3.y, vector2_equals(v1, v2));
return 0;
}import std.stdio : writeln;
struct Vector2 {
int x, y;
Vector2 opBinary(string op : "+")(Vector2 other) const {
return Vector2(x + other.x, y + other.y);
}
bool opEquals(const Vector2 other) const {
return x == other.x && y == other.y;
}
string toString() const {
import std.format : format;
return format("(%d,%d)", x, y);
}
}
void main() {
Vector2 v1 = Vector2(1, 2), v2 = Vector2(3, 4);
writeln(v1 + v2); // calls opBinary!"+"
writeln(v1 == v2); // calls opEquals
}Other hooks follow the same pattern:
opCmp for ordering comparisons, opIndex for [], opCall for (). The toString method is called automatically by writeln and string conversion — no separate format machinery needed.@property accessors
The
@property attribute makes a method callable without parentheses — temp.celsius calls the getter, temp.celsius = 100 calls the setter.#include <stdio.h>
typedef struct {
double _celsius;
} Temperature;
// Getter/setter as free functions
double temperature_get(const Temperature *t) { return t->_celsius; }
void temperature_set(Temperature *t, double c) {
if (c >= -273.15) t->_celsius = c;
}
double temperature_fahrenheit(const Temperature *t) {
return t->_celsius * 9.0/5.0 + 32.0;
}
int main(void) {
Temperature temp = {100.0};
printf("%.1f C = %.1f F\n",
temperature_get(&temp), temperature_fahrenheit(&temp));
return 0;
}import std.stdio : writefln;
struct Temperature {
private double _celsius;
@property double celsius() const { return _celsius; }
@property void celsius(double c) {
if (c >= -273.15) _celsius = c;
}
@property double fahrenheit() const {
return _celsius * 9.0/5.0 + 32.0;
}
}
void main() {
Temperature temp;
temp.celsius = 100.0; // calls setter
writefln("%.1f C = %.1f F", temp.celsius, temp.fahrenheit);
}This lets you start with a public field and later replace it with a computed property without changing any calling code — something C's free-function getters and setters can never offer.
Classes & OOP
Classes are reference types
D classes are reference types —
new Person(...) allocates on the GC heap and returns a reference, and passing the variable copies the reference, not the object.#include <stdio.h>
#include <stdlib.h>
typedef struct {
char name[64];
int age;
} Person;
Person *person_new(const char *name, int age) {
Person *person = malloc(sizeof(Person));
// snprintf for safety
__builtin_snprintf(person->name, 64, "%s", name);
person->age = age;
return person;
}
int main(void) {
Person *alice = person_new("Alice", 30);
printf("%s is %d\n", alice->name, alice->age);
free(alice);
return 0;
}import std.stdio : writefln;
class Person {
string name;
int age;
this(string name, int age) {
this.name = name;
this.age = age;
}
void greet() {
writefln("%s is %d", name, age);
}
}
void main() {
Person alice = new Person("Alice", 30);
alice.greet();
// No free() — GC handles deallocation
}The GC collects unreachable objects automatically — there is no
free. For value-type semantics, use a struct instead.Inheritance and override
D supports single inheritance with
class Child : Parent, generating automatically the vtable dispatch the C side wires up by hand.#include <stdio.h>
#include <math.h>
typedef struct {
double (*area)(void *self);
} Shape;
typedef struct { Shape base; double radius; } Circle;
typedef struct { Shape base; double width, height; } Rect;
double circle_area(void *self) {
return 3.14159 * ((Circle *)self)->radius * ((Circle *)self)->radius;
}
double rect_area(void *self) {
Rect *r = (Rect *)self;
return r->width * r->height;
}
int main(void) {
Circle c = {{circle_area}, 5.0};
Rect r = {{rect_area}, 4.0, 6.0};
printf("%.2f %.2f\n", c.base.area(&c), r.base.area(&r));
return 0;
}import std.stdio : writefln;
import std.math : PI;
class Shape {
abstract double area() const;
void describe() const { writefln("area=%.2f", area()); }
}
class Circle : Shape {
double radius;
this(double r) { radius = r; }
override double area() const { return PI * radius * radius; }
}
class Rect : Shape {
double width, height;
this(double w, double h) { width = w; height = h; }
override double area() const { return width * height; }
}
void main() {
Shape[] shapes = [new Circle(5.0), new Rect(4.0, 6.0)];
foreach (shape; shapes) shape.describe();
}The
override keyword is required when overriding a virtual method — forgetting it (or misspelling the method name) is a compile error. abstract marks a method with no body that subclasses must implement.Interfaces
D interfaces are pure abstract types — no member fields, no method bodies — and a class can implement several at once.
#include <stdio.h>
// C simulates interfaces via structs of function pointers
typedef struct {
void (*serialize)(void *self, char *buffer, int size);
int (*size)(void *self);
} Serializable;
typedef struct {
Serializable iface;
int value;
} Number;
void number_serialize(void *self, char *buf, int size) {
__builtin_snprintf(buf, size, "%d", ((Number *)self)->value);
}
int number_size(void *self) { return 4; }
int main(void) {
Number n = {{number_serialize, number_size}, 42};
char buf[32];
n.iface.serialize(&n, buf, sizeof(buf));
printf("%s\n", buf);
return 0;
}import std.stdio : writeln;
import std.conv : to;
interface Serializable {
string serialize() const;
int byteSize() const;
}
interface Printable {
void print() const;
}
class Number : Serializable, Printable {
int value;
this(int v) { value = v; }
string serialize() const { return value.to!string; }
int byteSize() const { return int.sizeof; }
void print() const { writeln("Number: ", value); }
}
void main() {
Number num = new Number(42);
Serializable s = num;
writeln(s.serialize(), " (", s.byteSize(), " bytes)");
num.print();
}Interface references provide dynamic dispatch without knowing the concrete type at compile time — the role the hand-built function-pointer struct plays on the C side.
final and sealed classes
final can be applied to individual methods (preventing override in subclasses) or to an entire class (preventing any subclassing).#include <stdio.h>
// C has no way to prevent "inheritance" of struct-based types
typedef struct { int value; } SafeInt;
int main(void) {
SafeInt si = {42};
printf("%d\n", si.value);
return 0;
}import std.stdio : writeln;
class Base {
void greet() { writeln("Base"); }
final void identify() { writeln("I am Base"); }
}
class Child : Base {
override void greet() { writeln("Child"); }
// override void identify() { ... } // compile error: final
}
final class Sealed {
int value;
this(int v) { value = v; }
}
// class Extended : Sealed {} // compile error: cannot extend final class
void main() {
Child child = new Child();
child.greet();
child.identify(); // calls Base.identify via final dispatch
}A
final method is also a performance hint — the compiler can devirtualize the call and inline it. final class is equivalent to Java's final class.Templates
Function templates
D function templates put the type parameter in a second pair of parentheses before the argument list:
func(T)(T arg) — a type-safe replacement for C's generic macros.#include <stdio.h>
// C uses macros for generic functions — no type safety
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SWAP(T, a, b) do { T _tmp = a; a = b; b = _tmp; } while(0)
int main(void) {
printf("%d\n", MAX(3, 7));
printf("%.1f\n", MAX(3.5, 2.1));
int x = 1, y = 2;
SWAP(int, x, y);
printf("%d %d\n", x, y);
return 0;
}import std.stdio : writeln;
// Type parameter T — compiler generates a version for each type used
T max(T)(T a, T b) { return a > b ? a : b; }
void swap(T)(ref T a, ref T b) {
T temp = a;
a = b;
b = temp;
}
void main() {
writeln(max(3, 7)); // max!int — inferred
writeln(max(3.5, 2.1)); // max!double — inferred
int x = 1, y = 2;
swap(x, y);
writeln(x, " ", y);
}The compiler infers
T from the argument types — explicit instantiation (max!int(3, 7)) is rarely needed. Unlike macros, templates are type-safe, evaluated in the function's own scope, and show meaningful error messages.Template constraints
Template constraints — an
if (condition) after the parameter list — restrict which types a template accepts, something C macros cannot express at all.#include <stdio.h>
// C macros accept anything — no constraint possible
#define SUM_ARRAY(arr, len) ({ __typeof__(arr[0]) _s = 0; for (int _i = 0; _i < (len); _i++) _s += arr[_i]; _s; })
int main(void) {
int numbers[] = {1, 2, 3, 4, 5};
printf("%d\n", SUM_ARRAY(numbers, 5));
return 0;
}import std.stdio : writeln;
import std.traits : isNumeric;
// Template only accepted when T is a numeric type
T sumArray(T)(T[] arr) if (isNumeric!T) {
T total = 0;
foreach (x; arr) total += x;
return total;
}
void main() {
int[] integers = [1, 2, 3, 4, 5];
double[] doubles = [1.1, 2.2, 3.3];
writeln(sumArray(integers)); // 15
writeln(sumArray(doubles)); // 6.6
// sumArray(["a", "b"]); // compile error: constraint not satisfied
}The constraint must be evaluable at compile time, typically using traits from
std.traits: isNumeric, isIntegral, isFloatingPoint, isSomeString, isArray, and more. A failed constraint produces a readable "template is not callable" error.Compile-time function execution (CTFE)
CTFE (Compile-Time Function Execution): ordinary D functions run at compile time when used in a compile-time context — no special keyword, no separate metaprogramming dialect.
#include <stdio.h>
// C: only constant expressions at compile time (no function calls)
// Must use recursive macros or _Generic tricks
enum { FACTORIAL_5 = 1*2*3*4*5 }; // must hard-code
int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
int main(void) {
printf("compile-time: %d\n", FACTORIAL_5);
printf("runtime: %d\n", factorial(5));
return 0;
}import std.stdio : writeln;
// Ordinary function — runs at runtime OR compile time
int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
// enum forces compile-time evaluation (CTFE)
enum ctFactorial5 = factorial(5); // computed at compile time
enum ctFactorial10 = factorial(10);
void main() {
writeln("compile-time: ", ctFactorial5); // 120
writeln("compile-time: ", ctFactorial10); // 3628800
writeln("runtime: ", factorial(7));
}Declaring with
enum forces the value to be computed at compile time. The same function works at both runtime and compile time, unlike C where a compile-time constant must be hard-coded or macro-generated.String mixins for code generation
D's
mixin(string) injects D source code into the surrounding scope at compile time — code generation in the language itself, replacing C's ## token-pasting macros.#include <stdio.h>
// C uses the preprocessor for code generation — no type safety
#define DEFINE_GETTER(type, field) \
type get_##field(void) { return global_##field; }
int global_count = 0;
DEFINE_GETTER(int, count)
int main(void) {
global_count = 42;
printf("%d\n", get_count());
return 0;
}import std.stdio : writeln;
// Mixin injects D source code generated at compile time
string defineCounter(string name) {
return "int " ~ name ~ " = 0;"
~ "void increment_" ~ name ~ "() { " ~ name ~ "++; }"
~ "int get_" ~ name ~ "() { return " ~ name ~ "; }";
}
mixin(defineCounter("requests"));
mixin(defineCounter("errors"));
void main() {
increment_requests();
increment_requests();
increment_errors();
writeln("requests=", get_requests(), " errors=", get_errors());
}The string is produced by an ordinary function running under CTFE, so it has full access to the type system. Unlike preprocessor text substitution, a mixin is parsed and type-checked after injection.
Memory & Scope Guards
Garbage collection and new
D's garbage collector makes memory management automatic for typical code —
new allocates on the GC heap, and the runtime collects objects once they become unreachable.#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct { char name[64]; int score; } Player;
Player *player_new(const char *name, int score) {
Player *player = malloc(sizeof(Player));
strncpy(player->name, name, 63);
player->score = score;
return player;
}
int main(void) {
Player *player = player_new("Alice", 100);
printf("%s: %d\n", player->name, player->score);
free(player); // must not forget!
return 0;
}import std.stdio : writefln;
class Player {
string name;
int score;
this(string name, int score) { this.name = name; this.score = score; }
}
void main() {
Player player = new Player("Alice", 100);
writefln("%s: %d", player.name, player.score);
// No free() — the GC reclaims the object once it becomes unreachable
}Collection happens at the GC's discretion, not at scope exit. Manual allocation remains available via
core.stdc.stdlib.malloc/free or std.experimental.allocator for performance-critical sections.scope(exit) for guaranteed cleanup
scope(exit) registers a cleanup action that runs when the current scope exits — by normal return, exception, or any other path — placed right next to the acquisition it cleans up.#include <stdio.h>
#include <stdlib.h>
int process(int fail) {
FILE *file = fopen("/dev/null", "r");
if (!file) return -1;
int *buffer = malloc(256);
if (!buffer) { fclose(file); return -1; }
if (fail) { free(buffer); fclose(file); return -1; }
printf("processing\n");
free(buffer);
fclose(file);
return 0;
}
int main(void) {
process(0);
process(1);
return 0;
}import std.stdio : writeln;
import std.exception : enforce;
import core.stdc.stdio : fopen, fclose;
int process(bool fail) {
auto file = fopen("/dev/null", "r");
enforce(file !is null, "could not open file");
scope(exit) fclose(file); // always runs, even on exception
auto buffer = new ubyte[256];
scope(exit) writeln("buffer released");
enforce(!fail, "simulated failure");
writeln("processing");
return 0;
}
void main() {
process(false);
writeln("---");
try { process(true); } catch (Exception e) writeln("caught: ", e.msg);
}This eliminates C's duplicated cleanup code on every return path; it plays the role of RAII destructors or Go's
defer. scope(success) runs only on normal exit; scope(failure) only on exception (next example).scope(success) and scope(failure)
scope(success) runs only when the scope exits normally; scope(failure) runs only when an exception propagates out.#include <stdio.h>
#include <setjmp.h>
// C has no equivalent — requires manual flag tracking
int main(void) {
int success = 0;
// ... (no clean built-in mechanism)
success = 1;
if (success) printf("committed\n");
else printf("rolled back\n");
return 0;
}import std.stdio : writeln;
void transfer(bool fail) {
writeln("Starting transfer");
scope(success) writeln("Committed!");
scope(failure) writeln("Rolled back!");
scope(exit) writeln("Transfer done.");
if (fail) throw new Exception("network error");
writeln("Transferred funds");
}
void main() {
transfer(false);
writeln("---");
try { transfer(true); } catch (Exception e) writeln("Error: ", e.msg);
}Together they model a transaction pattern: prepare in the body, commit in
scope(success), roll back in scope(failure) — without wrapping everything in a try/catch. C offers nothing comparable short of manual flag tracking.Manual allocation with C stdlib
C's
malloc/free remain fully available in D via core.stdc.stdlib, for sections that must avoid GC pressure entirely.#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *numbers = malloc(5 * sizeof(int));
for (int i = 0; i < 5; i++) numbers[i] = i * i;
for (int i = 0; i < 5; i++) printf("%d ", numbers[i]);
printf("\n");
free(numbers);
return 0;
}import std.stdio : writeln, write;
import core.stdc.stdlib : malloc, free;
void main() {
// Manual allocation — bypasses GC entirely
int *numbers = cast(int*) malloc(5 * int.sizeof);
scope(exit) free(numbers);
foreach (i; 0..5) numbers[i] = i * i;
foreach (i; 0..5) write(numbers[i], " ");
writeln();
}The
cast(int*) is required because malloc returns void* and D, unlike C, does not implicitly convert it. The @nogc function attribute prevents accidental GC allocation in code that must remain GC-free.Ranges & Algorithms
filter and map
D's
std.algorithm functions accept any range and return lazy ranges — the pipeline below allocates nothing until .array() collects the results.#include <stdio.h>
int main(void) {
int numbers[] = {1, 2, 3, 4, 5, 6, 7, 8};
int count = 8;
// Filter evens and square them
int results[8];
int result_len = 0;
for (int i = 0; i < count; i++) {
if (numbers[i] % 2 == 0)
results[result_len++] = numbers[i] * numbers[i];
}
for (int i = 0; i < result_len; i++) printf("%d ", results[i]);
printf("\n");
return 0;
}import std.stdio : writeln;
import std.algorithm : filter, map;
import std.array : array;
void main() {
int[] numbers = [1, 2, 3, 4, 5, 6, 7, 8];
// Lazy pipeline — no intermediate arrays until .array()
int[] results = numbers
.filter!(x => x % 2 == 0)
.map!(x => x * x)
.array();
writeln(results); // [4, 16, 36, 64]
}The
! is the template instantiation operator — filter!(x => x % 2 == 0) specializes filter with the lambda predicate at compile time.reduce and fold
Common reductions have named shortcuts —
.sum(), .minElement(), .maxElement() — and reduce handles the general fold.#include <stdio.h>
int main(void) {
int numbers[] = {1, 2, 3, 4, 5};
int sum = 0, product = 1, max_val = numbers[0];
for (int i = 0; i < 5; i++) {
sum += numbers[i];
product *= numbers[i];
if (numbers[i] > max_val) max_val = numbers[i];
}
printf("sum=%d product=%d max=%d\n", sum, product, max_val);
return 0;
}import std.stdio : writeln;
import std.algorithm : reduce, maxElement, sum;
void main() {
int[] numbers = [1, 2, 3, 4, 5];
int total = numbers.sum();
int product = numbers.reduce!((a, b) => a * b);
int biggest = numbers.maxElement();
writeln("sum=", total, " product=", product, " max=", biggest);
}std.algorithm.reduce is a left fold; the reduce!((a, b) => ...) form accepts any binary function as a template argument, specialized at compile time for zero overhead.Sort and find
std.algorithm.sort sorts in place with a type-checked comparison — no void* comparator function.#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int compare_int(const void *a, const void *b) {
return *(int *)a - *(int *)b;
}
int main(void) {
int numbers[] = {5, 3, 8, 1, 9, 2};
qsort(numbers, 6, sizeof(int), compare_int);
for (int i = 0; i < 6; i++) printf("%d ", numbers[i]);
printf("\n");
return 0;
}import std.stdio : writeln;
import std.algorithm : sort, find, canFind;
void main() {
int[] numbers = [5, 3, 8, 1, 9, 2];
numbers.sort();
writeln(numbers); // [1, 2, 3, 5, 8, 9]
// sort with custom comparator
numbers.sort!((a, b) => a > b);
writeln(numbers); // [9, 8, 5, 3, 2, 1]
// find returns a slice starting at the found element
int[] found = numbers.find(5);
writeln(found[0]); // 5
writeln(numbers.canFind(3)); // true
}The default sort is unstable (an introsort hybrid); the template predicate
sort!((a, b) => a > b) inverts ordering inline. find returns the remaining range starting at the match — a D idiom that avoids returning an index that would need bounds checking.Lazy ranges with iota
std.range.iota(start, stop, step) generates integers lazily — each value is computed on demand, and no backing array is ever allocated.#include <stdio.h>
int main(void) {
// Must compute and store — no lazy evaluation
int squares[10];
long long sum = 0;
for (int i = 0; i < 10; i++) {
squares[i] = i * i;
sum += squares[i];
}
printf("sum of squares 0..9 = %lld\n", sum);
return 0;
}import std.stdio : writeln, write;
import std.range : iota;
import std.algorithm : map, sum;
void main() {
// iota is lazy — no array allocated
auto total = iota(0, 10)
.map!(i => i * i)
.sum();
writeln("sum of squares 0..9 = ", total); // 285
// Equivalent foreach
foreach (i; iota(0, 100, 10)) { // 0, 10, 20, ... 90
write(i, " ");
}
writeln();
}Chaining
.map and .sum on a lazy range means the entire pipeline runs in a single pass with no temporary storage — the same work as the C loop, expressed declaratively.Error Handling
Exceptions
D replaces C's error codes with exceptions, using
try/catch/finally identical to Java and C#.#include <stdio.h>
#include <string.h>
// C uses error codes — caller must check every return value
int safe_divide(int a, int b, int *result) {
if (b == 0) return -1; // error code
*result = a / b;
return 0;
}
int main(void) {
int result;
if (safe_divide(10, 2, &result) == 0)
printf("%d\n", result);
if (safe_divide(10, 0, &result) != 0)
printf("error: division by zero\n");
return 0;
}import std.stdio : writeln;
int safeDivide(int a, int b) {
if (b == 0) throw new Exception("division by zero");
return a / b;
}
void main() {
writeln(safeDivide(10, 2)); // 5
try {
writeln(safeDivide(10, 0));
} catch (Exception e) {
writeln("caught: ", e.msg);
} finally {
writeln("always runs");
}
}Exception represents recoverable errors; Error (a separate hierarchy) represents unrecoverable failures like assertion errors and out-of-memory. Catching Exception does not catch Error, which propagates until the program terminates.Exception hierarchy
Custom exceptions are classes inheriting from
Exception, giving errors structure and hierarchy where C has only integer errno codes.#include <stdio.h>
#include <errno.h>
#include <string.h>
// C error hierarchy is errno codes — no inheritance
int main(void) {
FILE *file = fopen("nonexistent.txt", "r");
if (!file) {
printf("error %d: %s\n", errno, strerror(errno));
}
return 0;
}import std.stdio : writeln;
class AppException : Exception {
int code;
this(string msg, int code) {
super(msg);
this.code = code;
}
}
class NetworkException : AppException {
this(string msg) { super(msg, 503); }
}
void riskyOperation(bool fail) {
if (fail) throw new NetworkException("connection refused");
}
void main() {
try {
riskyOperation(true);
} catch (NetworkException e) {
writeln("network error (", e.code, "): ", e.msg);
} catch (AppException e) {
writeln("app error: ", e.msg);
} catch (Exception e) {
writeln("unexpected: ", e.msg);
}
}The
super(msg) call passes the message to the base class constructor. Catch blocks are checked top-to-bottom — put more specific types before more general ones. The Throwable root class covers both Exception and Error.enforce for runtime assertions
std.exception.enforce(condition, message) throws an Exception if the condition is false — validation that cannot be compiled away.#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
double safe_sqrt(double x) {
if (x < 0) {
fprintf(stderr, "sqrt of negative: %f\n", x);
exit(1); // or return an error code — no good option in C
}
return __builtin_sqrt(x);
}
int main(void) {
printf("%.2f\n", safe_sqrt(16.0));
// safe_sqrt(-1.0) would print to stderr and exit(1)
return 0;
}import std.stdio : writeln;
import std.exception : enforce;
import std.math : sqrt;
import std.conv : to;
double safeSqrt(double x) {
enforce(x >= 0, "sqrt of negative: " ~ x.to!string);
return sqrt(x);
}
void main() {
writeln(safeSqrt(16.0)); // 4
try {
writeln(safeSqrt(-1.0));
} catch (Exception e) {
writeln("Error: ", e.msg);
}
}Unlike
assert, enforce is never disabled by -release, making it appropriate for runtime input validation. Use assert for invariants you control; use enforce for external conditions you cannot guarantee.nothrow functions
nothrow is a compiler-verified guarantee that a function does not propagate exceptions — restoring, as an explicit checked promise, the no-exceptions world C code always assumes.#include <stdio.h>
#include <setjmp.h>
// C has no way to declare a function cannot throw — everything is nothrow by default
int clamp(int value, int lo, int hi) {
if (value < lo) return lo;
if (value > hi) return hi;
return value;
}
int main(void) {
printf("%d\n", clamp(15, 0, 10));
return 0;
}import std.stdio : writeln;
// nothrow: compiler verifies this function cannot throw
nothrow int clamp(int value, int lo, int hi) {
if (value < lo) return lo;
if (value > hi) return hi;
return value;
}
// pure + nothrow + @safe: maximum constraint
pure nothrow @safe int square(int x) { return x * x; }
void main() {
writeln(clamp(15, 0, 10)); // 10
writeln(square(7)); // 49
}Code inside a
nothrow function cannot call throwing functions unless the call is wrapped in a try block. The compiler can then skip exception-handling overhead at call sites, and the attribute matters especially for D functions exposed to C callers, which cannot handle a D exception.Contracts & Testing
in contracts (preconditions)
D's
in contract — in (condition, "message") between the signature and the body — is a precondition checked on every call in debug builds.#include <stdio.h>
#include <assert.h>
// C: manual precondition checks with assert or if
int factorial(int n) {
assert(n >= 0 && "n must be non-negative");
if (n <= 1) return 1;
return n * factorial(n - 1);
}
int main(void) {
printf("%d\n", factorial(5));
return 0;
}import std.stdio : writeln;
int factorial(int n)
in (n >= 0, "n must be non-negative")
{
if (n <= 1) return 1;
return n * factorial(n - 1);
}
void main() {
writeln(factorial(5)); // 120
writeln(factorial(0)); // 1
// factorial(-1); // AssertError at runtime in debug mode
}Contracts are disabled by
-release, so they document the function's expectations as executable specification at zero production cost. The older multi-statement form uses in { assert(condition); } before do { ... }.out contracts (postconditions)
The
out (result; condition) contract binds the return value to result and checks the condition after the function returns — a postcondition, the mirror of in.#include <stdio.h>
#include <assert.h>
// Manual postcondition — must capture return value
int absolute_value(int x) {
int result = x < 0 ? -x : x;
assert(result >= 0 && "result must be non-negative");
return result;
}
int main(void) {
printf("%d %d\n", absolute_value(-7), absolute_value(3));
return 0;
}import std.stdio : writeln;
int absoluteValue(int x)
out (result; result >= 0, "result must be non-negative")
{
return x < 0 ? -x : x;
}
// Both in and out contracts together
double ratio(double a, double b)
in (b != 0.0, "divisor cannot be zero")
out (result; result >= 0.0)
{
return (a / b < 0) ? -(a / b) : (a / b);
}
void main() {
writeln(absoluteValue(-7)); // 7
writeln(ratio(10.0, -3.0)); // 3.333...
}in and out contracts are inherited by overriding methods: an override can only weaken preconditions and strengthen postconditions, the standard design-by-contract rule.Built-in unittest blocks
Testing is built into the language:
unittest blocks live next to the code they test and run automatically before main when compiled with -unittest.#include <stdio.h>
#include <assert.h>
// C has no built-in test framework — use external libraries
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
// Tests are just main() or a separate test binary
int main(void) {
assert(add(2, 3) == 5);
assert(add(-1, 1) == 0);
assert(multiply(3, 4) == 12);
printf("all tests passed\n");
return 0;
}import std.stdio : writeln;
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
// unittest blocks live next to the code they test
unittest {
assert(add(2, 3) == 5);
assert(add(-1, 1) == 0);
}
unittest {
assert(multiply(3, 4) == 12);
assert(multiply(0, 99) == 0);
}
void main() {
// -unittest flag compiles + runs unittest blocks before main
writeln("all tests passed, running main");
writeln(add(10, 20));
}No test runner framework is needed. Each module can have multiple
unittest blocks anywhere in the file, and a failing assertion prints the file and line number.struct/class invariants
An
invariant block is checked automatically before and after every public method call — the consistency assertion C code must remember to repeat after each mutation.#include <stdio.h>
#include <assert.h>
typedef struct {
int width, height;
} Rectangle;
void rectangle_set_width(Rectangle *r, int w) {
assert(w > 0);
r->width = w;
// Must remember to check invariant manually after every mutation
assert(r->width > 0 && r->height > 0);
}
int main(void) {
Rectangle rect = {5, 3};
rectangle_set_width(&rect, 10);
printf("%d x %d\n", rect.width, rect.height);
return 0;
}import std.stdio : writeln;
struct Rectangle {
int width, height;
this(int width, int height) {
this.width = width;
this.height = height;
}
void setWidth(int w) { width = w; }
// invariant is checked on entry and exit of every public method
invariant {
assert(width > 0, "width must be positive");
assert(height > 0, "height must be positive");
}
int area() const { return width * height; }
}
void main() {
Rectangle rect = Rectangle(5, 3);
rect.setWidth(10);
writeln(rect.width, " x ", rect.height, " area=", rect.area());
}The object therefore can never be observed in an invalid state after a method call in debug builds. Like contracts, invariants are disabled by
-release.C Interop
Calling C functions
extern(C) declares a function with C linkage and ABI — the D compiler calls it with the C calling convention and an unmangled name.#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
char *copy = strdup("hello from C");
printf("%s (len=%zu)\n", copy, strlen(copy));
free(copy);
return 0;
}import std.stdio : writeln;
// Declare C functions explicitly
extern(C) {
char* strdup(const(char)* s);
size_t strlen(const(char)* s);
void free(void* ptr);
}
void main() {
import std.string : toStringz;
char* copy = strdup("hello from C".toStringz);
scope(exit) free(copy);
writeln(copy[0..strlen(copy)], " (len=", strlen(copy), ")");
}The C standard library is linked by default, so any libc function can be declared and called this way. The
core.stdc.* modules pre-declare the most common C stdlib headers (next example).core.stdc modules
The
core.stdc.* modules are D's pre-built bindings to C's standard library — core.stdc.stdio, core.stdc.stdlib, core.stdc.string, core.stdc.math — preferable to hand-writing extern(C) declarations.#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
int main(void) {
double root = sqrt(144.0);
char buffer[64];
snprintf(buffer, sizeof(buffer), "sqrt(144) = %.1f", root);
printf("%s\n", buffer);
return 0;
}import std.stdio : writeln;
import core.stdc.math : sqrt;
import core.stdc.stdio : snprintf;
void main() {
double root = sqrt(144.0);
char[64] buffer;
snprintf(buffer.ptr, buffer.length, "sqrt(144) = %.1f", root);
// Convert C string to D string
import std.string : fromStringz;
writeln(buffer.ptr.fromStringz);
}The
fromStringz function converts a null-terminated char* to a D string.C-compatible structs
D structs are C-compatible by design: fields are laid out in declaration order with C's alignment rules, so a plain D struct can be passed to and from C code via pointers.
#include <stdio.h>
// Ordinary C struct — layout guaranteed by the C ABI
typedef struct {
int x;
int y;
float scale;
} Transform;
void print_transform(const Transform *t) {
printf("Transform(%d, %d, %.2f)\n", t->x, t->y, t->scale);
}
int main(void) {
Transform t = {10, 20, 1.5f};
print_transform(&t);
return 0;
}import std.stdio : writeln;
// D structs already use C-compatible field layout
extern(C) struct Transform {
int x;
int y;
float scale;
}
// D function callable from C
extern(C) void printTransform(const(Transform)* t) {
writeln("Transform(", t.x, ", ", t.y, ", ", t.scale, ")");
}
void main() {
Transform transform = Transform(10, 20, 1.5f);
printTransform(&transform);
}D never reorders struct fields, so
extern(C) on the struct is documentation rather than a layout change — the attribute matters on functions, where it selects the C calling convention and an unmangled symbol name callable directly from separately compiled C code.