PONY λ M2 Modula-2

C.CodeCompared.To/D

An interactive executable cheatsheet comparing C and D

C17 (GCC) D 2.112
Hello World & Modules
Hello, World
#include <stdio.h> int main(void) { printf("Hello, World!\n"); return 0; }
import std.stdio; void main() { writeln("Hello, World!"); }
D uses import modules instead of #include headers — the compiler resolves symbols from compiled .di interface files, eliminating the need for header guards. writeln appends a newline automatically. The main function returns void by default and implicitly exits with code 0.
Selective imports
#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); }
D allows selective imports (import module : name) that bring only the named symbols into scope. 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
// 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.d
D ships with three compilers: DMD (reference, fastest compile times), LDC (LLVM-based, best runtime performance), and GDC (GCC backend). The -unittest flag compiles and runs all unittest blocks before main. The -release flag disables contracts and asserts for production builds.
Formatted output
#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); }
D's writefln uses the same %d/%s/%f format verbs as C's printf, so the transition is friction-free. The std.format module also provides a Python-style format("%s is %d", name, age) function that returns a string.
Variables & Types
Fixed-size integer types
#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); }
D's int is always 32-bit, long always 64-bit, byte always 8-bit signed, ubyte always 8-bit unsigned — no platform variation. C's long is 32-bit on Windows 64-bit and 64-bit on Linux 64-bit, which is why stdint.h exists. D eliminates this portability hazard at the type level.
Type inference with auto
#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 in D is true type inference — the compiler deduces the exact type from the initializer. Unlike C++'s auto, D's auto is available for any variable including loop variables. The inferred type is fixed at compile time; auto is not a dynamic type.
immutable vs const
#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 distinguishes const (a read-only view — the underlying data may still change via another reference) from immutable (transitive deep immutability — the data cannot be changed by anyone). D strings are immutable(char)[], which is why they can be safely shared across threads without locks.
Type aliases with alias
#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, ")"); }
alias in D replaces C's typedef and is more powerful: it can alias templates, function types, and even template parameters. Unlike typedef, alias creates a true transparent alias — the aliased type and the original are identical in the type system, not just compatible.
typeof and type properties
#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); }
Every D type exposes compile-time properties via dot syntax: .sizeof, .min, .max, .init (the zero-value), .stringof (the type name as a string). typeof(expr) produces the type of an expression at compile time, analogous to C++ decltype.
Arrays & Slices
Static arrays
#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]); }
D static arrays put the size before the name (int[5] rather than int arr[5]), making the type read left-to-right: "array of 5 ints." The .length property replaces the sizeof/sizeof division. Static arrays in D are value types — assigning one copies all elements.
Dynamic arrays
#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); }
D dynamic arrays (int[]) are GC-managed — no malloc/free needed. The ~= operator appends a single element or another array. Internally a dynamic array is a fat pointer: a length and a pointer to GC-managed memory. Passing one to a function passes both the length and pointer by value.
Array slices
#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] }
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. The special $ symbol means "the length of the array being sliced." Slices are the primary reason D rarely needs pointer arithmetic.
Concatenation and copy
#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 }
The ~ operator concatenates arrays or appends an element, returning a new array. .dup makes a mutable copy of an array (or string). .idup makes an immutable copy. Since slices share memory, .dup is needed when you want an independent copy to modify.
Iterating with index
#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); } }
D's foreach (index, element; collection) form provides both index and value without manual bookkeeping. The variable before the semicolon is the index; the one after is the element. Both variables are typed by inference. Omitting the index (foreach (color; colors)) iterates values only.
Strings
String type
#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 }
D's string type is immutable(char)[] — a slice of immutable UTF-8 bytes. There is no null terminator; .length is always accurate. 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
#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()); }
String concatenation with ~ returns a new string without mutating either operand. The std.string module provides toUpper, toLower, strip, split, indexOf, and dozens of other operations. All string operations work on string, char[], and wstring uniformly.
String conversion
#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 }
std.conv.to is a single generic function that converts between any compatible types: strings to numbers, numbers to strings, enums to strings, and more. It throws ConvException on failure rather than silently returning 0 like atoi. The exclamation mark (to!int) is D's template instantiation syntax.
Building strings with format
#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); }
std.format.format is the string-building equivalent of snprintf — it returns a string rather than writing to a buffer, so 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
#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(); }
D's 0..5 is a built-in integer range literal; foreach (i; 0..5) iterates 0, 1, 2, 3, 4 without creating a range object. foreach_reverse iterates from high to low. For non-integer ranges, std.range.iota(start, stop, step) provides strided ranges.
Switch without fallthrough
#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; }
D's switch requires an explicit break or goto case in every case — accidental fallthrough is a compile error, eliminating a classic C bug. 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
#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 } } } }
D supports labeled break and continue statements — the label is placed before the loop, and break label exits that specific loop. This replaces C's common pattern of using a flag variable or goto to escape nested loops. continue outer similarly skips to the next iteration of the outer loop.
static if for compile-time branching
#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"); } }
static if is a compile-time conditional that operates on real D expressions, not preprocessor text. The condition must be evaluable at compile time, and only the selected branch is compiled. Unlike #if, static if can appear inside functions, structs, and templates, and can inspect type properties and template parameters.
Functions
Default parameter values
#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."); }
D supports default parameter values directly in the function signature — no duplicate function needed. Default values must be compile-time constants or expressions. Parameters with defaults must come after parameters without defaults. Callers can still pass arguments positionally.
Function overloading
#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"); }
D supports full function overloading — multiple functions with the same name, differentiated by parameter types. 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
#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); }
Universal Function Call Syntax (UFCS) means any free function f(x, args) can be called as x.f(args). 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
#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 }
D supports fully portable nested functions — functions defined inside other functions, invisible outside their enclosing scope. Nested functions can access and modify variables from the enclosing function (they are closures when the enclosing variables need to outlive the call). This is a core language feature, not a compiler extension.
Function attributes
#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)); }
D function attributes are verified by the compiler, not just hints: pure means no global state access (the compiler enforces this); @safe means no pointer casts, no undefined behavior; nothrow means no exceptions; @nogc means no GC allocations. These attributes compose — a @safe pure nothrow function is maximally constrained and highly optimizable.
Structs
Structs with methods
#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) }
D structs are value types (like C structs) but can have methods, constructors, and operator overloads. 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
#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()); }
D structs use this(params) as the constructor syntax. The inline contract in (condition, "message") on the constructor is checked at runtime in debug builds and skipped in release builds (-release). Unlike C, the constructor is called with the struct name as a function: Rectangle(5, 3).
Operator overloading
#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 }
D uses template operator methods: opBinary(string op : "+") overloads +, opEquals overloads ==, opCmp overloads ordering comparisons, opIndex overloads [], opCall overloads (). The toString method is called automatically by writeln and string conversion — no separate format machinery needed.
@property accessors
#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); }
The @property attribute makes a method callable without parentheses — temp.celsius calls the getter, temp.celsius = 100 calls the setter. This lets you start with a public field and later replace it with a computed property without changing any calling code, the same pattern as Ruby's attr_accessor → custom getter/setter refactor.
Classes & OOP
Classes are reference types
#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 }
D classes are reference types — new Person(...) allocates on the GC heap and returns a reference. Passing a class variable copies the reference, not the object (unlike a struct, which copies the value). The GC collects unreachable objects automatically. For value-type semantics, use a struct.
Inheritance and override
#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(); }
D supports single inheritance with the class Child : Parent syntax. The override keyword is required when overriding a virtual method — forgetting it (or misspelling the method name) is a compile error, preventing a common C++ bug. abstract marks a method with no body that subclasses must implement.
Interfaces
#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(); }
D interfaces are pure abstract types — all methods must be overridden, and an interface cannot have member fields or method bodies. A class can implement multiple interfaces. Interface references provide dynamic dispatch without knowing the concrete type at compile time.
final and sealed classes
#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 }
final can be applied to individual methods (preventing override in subclasses) or to an entire class (preventing any subclassing). 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 or Rust's sealed pattern.
Templates
Function templates
#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); }
D function templates use T as the type parameter in a second pair of parentheses before the argument list: func(T)(T arg). The compiler infers T from the argument types — explicit instantiation (max!int(3, 7)) is rarely needed. Unlike C macros, templates are type-safe, evaluated in the function's own scope, and show meaningful error messages.
Template constraints
#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 }
Template constraints (if (condition) after the parameter list) restrict which types a template accepts. The constraint must be evaluable at compile time — typically using traits from std.traits: isNumeric, isIntegral, isFloatingPoint, isSomeString, isArray, etc. A failed constraint produces a readable "template is not callable" error.
Compile-time function execution (CTFE)
#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)); }
CTFE (Compile-Time Function Execution) means that ordinary D functions can run at compile time when used in a compile-time context — no special keyword needed. Using enum as a variable declaration forces the value to be computed at compile time. The same function works at both runtime and compile time, unlike C where compile-time constants require separate metaprogramming machinery.
String mixins for code generation
#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()); }
D's mixin(string) injects D source code into the surrounding scope at compile time — a hygienic alternative to C macros. The string is produced by an ordinary function or CTFE expression, 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
#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() — GC handles deallocation when player goes out of scope }
D's garbage collector makes memory management automatic for typical code — new allocates on the GC heap and the runtime collects unreachable objects. You can still use manual allocation via core.stdc.stdlib.malloc/free or std.experimental.allocator for performance-critical sections.
scope(exit) for guaranteed cleanup
#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); }
scope(exit) registers a cleanup action that runs when the current scope exits — whether by normal return, exception, or any other path. This eliminates the need to duplicate cleanup code across every return path, and is the D equivalent of RAII destructors or Go's defer. scope(success) runs only on normal exit; scope(failure) only on exception.
scope(success) and scope(failure)
#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); }
scope(success) runs only when the scope exits normally (no exception); scope(failure) runs only when an exception propagates out. Together they model a transaction pattern: prepare in the body, commit in scope(success), rollback in scope(failure) — without wrapping everything in a try/catch.
Manual allocation with C stdlib
#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(); }
D provides direct access to C's malloc/free via core.stdc.stdlib for performance-critical sections that must avoid GC pressure. The cast(int*) is required because D's malloc returns void*, the same as C. The @nogc function attribute prevents accidental GC allocation in code that must remain GC-free.
Ranges & Algorithms
filter and map
#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] }
D's std.algorithm functions accept any range (arrays, lazy ranges, custom types) and return lazy ranges themselves. The ! is the template instantiation operator — filter!(x => x % 2 == 0) specializes filter with the lambda predicate at compile time. Calling .array() at the end forces evaluation and collects results into a heap array.
reduce and 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 with an initial seed or two-argument accumulator. Common reductions have named shortcuts: .sum(), .minElement(), .maxElement(). The reduce!((a, b) => ...) form accepts any binary function as a template argument, evaluated at compile time for zero overhead.
Sort and find
#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 }
std.algorithm.sort sorts in place and uses an introsort hybrid (unstable). The template predicate sort!((a, b) => a > b) inverts ordering without a separate comparator function. 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
#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(); }
std.range.iota(start, stop, step) generates integers lazily — it computes each value on demand and never allocates a backing array. Chaining .map and .sum on a lazy range means the entire pipeline runs in a single pass with no temporary storage, equivalent to the C loop.
Error Handling
Exceptions
#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"); } }
D exceptions use try/catch/finally, identical to Java and C#. 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
#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); } }
Custom exceptions inherit from Exception (or another exception class). 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
#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); } }
std.exception.enforce(condition, message) throws an Exception if the condition is false — it is the "throw on false" counterpart to assert. Unlike assert, enforce is never disabled by -release and is appropriate for runtime input validation. Use assert for invariants you control; use enforce for external conditions you cannot guarantee.
nothrow functions
#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 }
nothrow is a compiler-verified guarantee that a function does not propagate exceptions. Code inside a nothrow function cannot call throwing functions (unless wrapped in a try block). This enables the compiler to skip exception-handling overhead in call sites and is required when interfacing with C code via extern(C).
Contracts & Testing
in contracts (preconditions)
#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 }
D's in contract (in (condition, "message")) is a precondition checked at the start of every function call in debug mode and disabled in release mode (-release). It documents the function's expectations as executable specification. The older multi-statement form uses in { assert(condition); } before do { ... }.
out contracts (postconditions)
#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... }
The out (result; condition) contract binds the return value to result and checks the condition after the function returns. This is the standard design-by-contract postcondition. in and out contracts are inherited by overriding methods: an override can only weaken preconditions and strengthen postconditions.
Built-in unittest blocks
#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)); }
unittest blocks are compiled only when -unittest is passed to the compiler. They run automatically before main — no test runner framework needed. Each module can have multiple unittest blocks anywhere in the file, typically adjacent to the function being tested. A failing assertion prints the file and line number.
struct/class invariants
#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()); }
An invariant block in a struct or class is checked automatically before and after every public method call in debug mode. This guarantees that the object is always in a consistent state — you cannot accidentally leave it in an invalid state after a method call. Like contracts, invariants are disabled by -release.
C Interop
Calling C functions
#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), ")"); }
extern(C) declares a function with C linkage and ABI — the D compiler will call it with the C calling convention rather than the D convention. 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.
core.stdc modules
#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 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, etc. These are preferable to hand-writing extern(C) declarations. The fromStringz function converts a null-terminated char* to a D string.
C-compatible structs
#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; // extern(C) ensures layout matches C's ABI 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); }
extern(C) struct makes a D struct use the C ABI field layout — this is required when passing structs to or from C code via pointers. Without extern(C), the D compiler may reorder or pad fields differently. An extern(C) function can be exported as a symbol callable directly from C code compiled separately.