The OpenD programming language
D is a general-purpose, high-level, multi-paradigm programming language. It was first released by Walter Bright in 2001 as a direct competitor to C++. It retains the familiarity of C-like syntax; however it is not backwards-compatible with C at the source code level. This decision (among others) has allowed D to avoid many of the problems which plague C and C++ to this day.
D is powerful and expressive, yet elegant and readable. Although it does not command a significant market share, it is a mature and dependable option for 2026 and beyond. OpenD is a variant of D with a less bureaucratic development model, saner defaults and fewer regressions than upstream D. It has replaced C for most new programs that I write, including the dynamic backend which served this page.
Advantages
- Fast compilation speed
D's grammar is almost context-free and the compiler does not require a symbol table to resolve ambiguities. - Compiler backends
The reference implementation, DMD, has a fast x86 backend but produces slower code. There is also an LLVM-based compiler known as LDC, which incorporates the DMD frontend. - foreach
The foreach loop construct enables iteration over a collection of objects while avoiding off-by-one errors. It achieves this by either using the length property of a slice, or by consuming an input range. - Nested functions
When you call a nested function in D, the activation record that is pushed to the stack will contain a static link to the enclosing scope, allowing full access to its context. This can be disabled by marking the function as static. - Function overloading
Functions in the same namespace can share the same name. The compiler will automatically select the appropriate function based on the parameters passed. - Operator overloading
Built-in operators can be overloaded by defining the required methods. It is not possible to define custom operators. There are some special operators, e.g. opCall overloads parentheses to turn objects into method calls, opCast overloads the cast() operator and opDispatch is a catch-all for unimplemented method calls. - Information hiding
D makes information hiding a useful technique beyond the rigid confines of object-oriented programming by redefining private to have module scope rather than object scope. - Automatic memory management
D is one of the few systems languages which use garbage collection as the primary dynamic memory management strategy. When you attempt to allocate memory from the GC heap, it will decide whether or not to run a collection cycle based on the ratio of currently used memory to the total reserved heap size, or if there is simply not enough reserved space available to meet the request. When a collection cycle is triggered, the GC will pause all threads and hijack the caller. The GC will then scan the stack for pointers to the heap. If this is the first collection cycle, a threadpool will be spawned. Valid pointers are divided between these threads and the objects are marked as active. Objects are tagged with an attribute indicating whether they contain additional pointers; those which do are scanned recursively until all active objects have been discovered. The marking threads then suspend execution until the next collection cycle. All other threads resume normal execution, excluding the caller. The GC then looks in the allocation table and frees any unmarked objects. The collection cycle is now complete and the GC relinquishes the calling thread. This type of GC is known as a conservative mark-sweep collector.
There is a widely held belief that programs which primarily rely on garbage collection are slower than programs which rely on manual memory management. This is false, at least in the average case. The research carried out by Benjamin Zorn (1992) confirms that programs using conservative garbage collection are primarily bound by memory overhead, not CPU overhead. When managing memory explicitly, an estimated 40% of total development time may be spent getting this correct (Xerox PARC CSL-84-7, 1985). Compounding the obvious unproductiveness of this endeavour, it is clear that a large proportion of vulnerabilities over the years in popular programs have been related to poor memory management. Many of these issues disappear when using a garbage collector.
It is also important to note that malloc/free are typically also unbounded and do not provide absolute latency guarantees. This is one of the reasons why dynamic memory management is often prohibited in safety-critical contexts. It is also important to consider that in languages where exception handling is the norm, garbage collection makes throwing exceptions cheaper in the average case as it is no longer necessary to execute destructors (as in C++). - Slices and bounds checking
Arrays and array references (slices) in D have associated lengths, allowing the compiler to emit bounds checking code for accesses. These checks eliminate security vulnerabilities caused by reading or writing past the end of an array. - Scope guards
It is possible to automatically execute a set of statements when program execution leaves a given scope. These scope() statements are executed in reverse order, similar to defer in other languages. They are executed no matter how the scope was exited, including by throwing exceptions or errors. It is possible to filter for these cases by using scope(success) and scope(failure). Scope guards ensure that the stack is unwound properly no matter where the throwable object is handled. - shared and immutable
Data can be marked as immutable in D using a type qualifier. This provides a stronger guarantee to the compiler that the data will not be mutated through any reference, and allows data to be shared across threads. Mutable data cannot be shared across threads unless it has been marked with the shared qualifier. With the nosharedaccess preview flag turned on, all non-atomic access to data marked as shared is blocked at compile-time. This qualifier can be cast away when using mutexes inside a nested scope. - Contracts and unit tests
D provides in {} and out {} blocks for preconditions and postconditions respectively. Unit tests are built into the language as a first-class feature. - Low-level manipulation
D supports all the operations you would typically expect to find in a systems programming language, such as inline assembly, raw pointers, malloc/free, atomics, bit manipulation, SIMD intrinsics and zero-overhead FFI. - Universal Function Call Syntax (UFCS)
Nested function calls can sometimes be unreadable. D solves this by eliminating the distinction between g(f(e(d(c(b(a)), 3)))) and a.b.c.d(3).e.f.g - Pipeline programming
Templates and static reflection are used to implement a set of conceptual interfaces known as ranges. Ranges allow algorithms to be executed against data objects without knowing the details of their implementation. The primitives required for implementing different kinds of ranges can be implemented either as object methods or free functions which take the object as a parameter. Algorithms are implemented as functions against ranges and return ranges. These can either be passed to additional algorithms (leveraging UFCS for pipeline programming) or consumed lazily, executing the operation(s). The overhead of using ranges compared to handwritten loops is close to zero. - Templates
D provides a generic programming facility at compile-time through the use of templates. These are instantiated using the ! operator instead of angle brackets, which keeps the grammar unambiguous. - Metaprogramming
D does not use macros or a preprocessor. Functions can be executed arbitrarily at compile-time (CTFE). Metaprogramming is achieved by using CTFE to generate code which can then be compiled using a mixin. - Conditional compilation
It is possible to select which portions of your source code get compiled and which do not. Conditional compilation based on the current platform is achieved using the version() statement. You can also execute arbitrary conditional logic at compile-time using static if, and do loop unrolling using static foreach. - Reflection
D provides static reflection via traits and dynamic reflection through runtime type information (RTTI).
Disadvantages
- Ecosystem
The D ecosystem is rather small and many of the packages that do exist are low-quality. You'll be better off sticking to your favourite C libraries. Using C from D is a lot more convenient than using C from C. - Object-oriented programming
D is multi-paradigm and does not necessarily force object-oriented design on the programmer. However, you may wish to avoid defining your own classes and interfaces. D provides excellent alternative facilities for generic programming which do not require you to pretend that data has "behaviour". - Resource Acquisition Is Initialisation (RAII)
D permits objects to have constructors and destructors. Aside from the hidden control flow problems they introduce, they enable the use of C++ style RAII. Correct use of RAII requires so-called "ownership" semantics and special static analysis features to enforce them. Pointers are divided into two categories: "unique" pointers and "shared" pointers. They really ought to be called useless pointers and slow pointers; the former because multiple references cannot be held simultaneously and the latter because it usually requires reference counting, which introduces overhead on accesses when the compiler cannot optimise it out. Reference counting may also poison cache lines and trigger pauses if one object's refcount reaches zero, causing another object's refcount to reach zero, and so on.
Getting started
The easiest way to get started is to download OpenD and read Ali Çehreli's book. It was written for upstream D, but most of the content is still relevant to OpenD. You may also find this Makefile template useful:
DC := ldc2
DEBUG := -unittest
AMD64_HARDEN := -fcf-protection=full
AARCH64_HARDEN := -mbranch-protection=standard
HARDEN := -relocation-model=pic
LD_HARDEN := -Xcc=-pie \
-L=-z -L=nodlopen \
-L=-z -L=noexecstack \
-L=-z -L=relro \
-L=-z -L=now \
-L=--as-needed \
-L=--no-copy-dt-needed-entries
DFLAGS := -O2 \
-g \
-d-debug \
--boundscheck=on \
--enable-asserts \
--enable-contracts \
-preview=nosharedaccess
LDFLAGS :=
OUT := a.out
CONFIG := $(DFLAGS) $(HARDEN) $(AMD64_HARDEN)
OBJS := $(patsubst src/%.d, obj/%.o, $(wildcard src/*.d))
.PHONY: all prepare clean
all: $(OUT)
prepare:
mkdir -p obj
clean:
rm -rf $(OUT) obj/*
$(OUT): $(OBJS)
$(DC) $(CONFIG) $(LD_HARDEN) -of $(OUT) $(OBJS) $(LDFLAGS)
objcopy --only-keep-debug $(OUT) $(OUT).debug
objcopy --strip-unneeded $(OUT)
objcopy --add-gnu-debuglink=$(OUT).debug $(OUT)
obj/main.o: src/main.d
$(DC) $(CONFIG) -I src -c src/main.d -of obj/main.o