The D 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. 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. - Multiple compiler backends
GDC (based on GCC), LDC (based on LLVM) and DMD (reference implementation). GDC and LDC use DMD's frontend. DMD has a faster x86 backend, but produces slower code. GDC is an official part of the GNU Compiler Collection. LDC is typically more up-to-date with the latest DMD versions. GDC follows the standard GCC release cadence and is more stable. - 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 synchronized {} block. - 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. - ImportC
Each D compiler contains an embedded C11 compiler which can be used to compile C code from D. Therefore, using a library with a C interface becomes trivial: simply add the required declarations to a file with a .c extension and import the file as if it was a native D module, supplying the relevant linker flags as necessary. Note that the system preprocessor will be run automatically when using DMD and LDC. This is not the case for GDC due to an architectural limitation. It is easily worked around by providing typedef maps for standard types. You can even create multiple ImportC units and select the correct one for your platform at compile-time using version(). ImportC does not support all GCC attributes, but you can usually just strip out unsupported ones. It does recognise most of the important compiler directives, such as #pragma pack(). - 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). - Strong community
The D community consists of a small but passionate group of organisations and individuals. The language is used commercially by Symmetry Investments, WekaIO, SARC and others. Symmetry is sponsoring the upcoming DConf 2026 in London this September, uniting D enthusiasts from around the world. In addition to the annual conference, there is a virtual meeting at the end of each month known as BeerConf. Development work continues both in upstream D and a fork started by Adam D. Ruppe known as OpenD. Code-sharing between the two projects remains prolific, improving the quality of each in a virtuous cycle.
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. - dub
dub is the official build system and package manager for D projects. It has terrible defaults for release builds and provides no significant benefit over other build systems. - Editions, DIP1000, @safe, @live
Upstream D intends to introduce an editions system similar to C#. Each module will contain a declaration indicating which edition of the language it was written in. This system will enable breaking changes in the future. A significant driver appears to be making D more memory-safe, primarily in non-GC contexts, by enabling these attributes and features by default. Luckily, it should be possible to disable this Rust-from-Temu soup by simply declaring an older language edition in each module. This is expected to work indefinitely, but if it doesn't, you can always port your code over to OpenD, where the editions approach has already been rejected. - 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 GDC from your OS package manager and read Ali Çehreli's book. You may also find this Makefile template useful:
DC := gdc
DEBUG := -g -fdebug -funittest
RELEASE := -s -fno-switch-errors
AMD64_HARDEN := -fcf-protection=full
AARCH64_HARDEN := -mbranch-protection=standard
HARDEN := -fPIE \
-fstack-clash-protection \
-fstack-protector-strong
LD_HARDEN := -pie \
-Wl,-z,nodlopen \
-Wl,-z,noexecstack \
-Wl,-z,relro \
-Wl,-z,now \
-Wl,--as-needed \
-Wl,--no-copy-dt-needed-entries
DFLAGS := -O2 \
-Wall \
-Wextra \
-Wdeprecated \
-fbounds-check=on \
-fpreview=nosharedaccess
LDFLAGS :=
OUT := a.out
CONFIG := $(DFLAGS) $(DEBUG) $(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) -o $(OUT) $(OBJS) $(LDFLAGS)
obj/main.o: src/main.d | src/external.c
$(DC) $(CONFIG) -I src -c src/main.d -o obj/main.o