D, also known as dlang, is a multi-paradigm system programming language created by Walter Bright at Digital Mars and released in 2001. Andrei Alexandrescu joined the design and development effort in 2007. Though it originated as a re-engineering of C++, D is now a very different language. As it has developed, it has drawn inspiration from other high-level programming languages. Notably, it has been influenced by Java, Python, Ruby, C#, and Eiffel.
The D language reference describes it as follows:
Features D is not source-compatible with C and C++ source code in general. However, any code that is legal in both C/C++ and D should behave in the same way.
Like C++, D has closures, anonymous functions, compile-time function execution, design by contract, ranges, built-in container iteration concepts, and type inference. D's declaration, statement and expression syntaxes also closely match those of C++.
Unlike C++, D also implements garbage collection, first-class arrays (std::array in C++ are technically not first-class), array slicing, nested functions and lazy evaluation. D uses Java-style single inheritance with interfaces and mixins rather than C++-style multiple inheritance.
D is a systems programming language. Like C++, and unlike application languages such as Java and C#, D supports low-level programming, including inline assembler. Inline assembler allows programmers to enter machine-specific assembly code within standard D code. System programmers use this method to access the low-level features of the processor that are needed to run programs that interface directly with the underlying hardware, such as operating systems and device drivers. Low-level programming is also used to write higher performance code than would be produced by a compiler.
D supports function overloading and operator overloading. Symbols (functions, variables, classes) can be declared in any order; forward declarations are not needed.
In D, text character strings are arrays of characters, and arrays in D are bounds-checked. D has first-class types for complex and imaginary numbers.
Programming paradigms D supports five main programming paradigms: Concurrent (actor model) Object-oriented Imperative Functional Metaprogramming
Imperative Imperative programming in D is almost identical to that in C. Functions, data, statements, declarations and expressions work just as they do in C, and the C runtime library may be accessed directly. However, unlike C, D's foreach loop construct allows looping over a collection. D also allows nested functions, which are functions that are declared inside another function, and which may access the enclosing function's local variables.
import std.stdio;
void main() { int multiplier = 10; int scaled(int x) { return x * multiplier;
foreach (i; 0 .. 10) { writefln("Hello, world %d! scaled = %d", i, scaled(i));
Object-oriented Object-oriented programming in D is based on a single inheritance hierarchy, with all classes derived from class Object. D does not support multiple inheritance; instead, it uses Java-style interfaces, which are comparable to C++'s pure abstract classes, and mixins, which separate common functionality from the inheritance hierarchy. D also allows the defining of static and final (non-virtual) methods in interfaces.
Interfaces and inheritance in D support covariant types for return types of overridden methods.
D supports type forwarding and optional custom dynamic dispatch.
Classes (and interfaces) in D can contain invariants which are automatically checked before and after entry to public methods, in accord with the design by contract methodology.
Many aspects of classes (and structs) can be introspected automatically at compile time (a form of reflective programming (reflection) using type traits) and at runtime (RTTI / TypeInfo), to facilitate generic code or automatic code generation (usually using compile-time techniques).
Functional D supports functional programming features such as function literals, closures, recursively-immutable objects and the use of higher-order functions. There are two syntaxes for anonymous functions, including a multiple-statement form and a "shorthand" single-expression notation:
int function(int) g; g = (x) { return x * x; }; // longhand g = (x) => x * x; // shorthand
There are two built-in types for function literals, function, which is simply a pointer to a stack-allocated function, and delegate, which also includes a pointer to the relevant stack frame, the surrounding ‘environment’, which contains the current local variables. Type inference may be used with an anonymous function, in which case the compiler creates a delegate unless it can prove that an environment pointer is not necessary. Likewise, to implement a closure, the compiler places enclosed local variables on the heap only if necessary (for example, if a closure is returned by another function, and exits that function's scope). When using type inference, the compiler will also add attributes such as pure and nothrow to a function's type, if it can prove that they apply.
Other functional features such as currying and common higher-order functions such as map, filter, and reduce are available through the standard library modules std.functional and std.algorithm.
import std.stdio, std.algorithm, std.range;
void main() { int[] a1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; int[] a2 = [6, 7, 8, 9];
// must be immutable to allow access from inside a pure function immutable pivot = 5;
int mySum(int a, int b) pure nothrow /* pure function */ { // ref to enclosing-scope if (b <= pivot) { return a + b; } else { return a;
// passing a delegate (closure) auto result = reduce!mySum(chain(a1, a2)); writeln("Result: ", result); // Result: 15
// passing a delegate literal result = reduce!((a, b) => (b <= pivot) ? a + b : a)(chain(a1, a2)); writeln("Result: ", result); // Result: 15 }
Alternatively, the above function compositions can be expressed using Uniform function call syntax (UFCS) for more natural left-to-right reading:
auto result = a1.chain(a2).reduce!mySum(); writeln("Result: ", result);
result = a1.chain(a2).reduce!((a, b) => (b <= pivot) ? a + b : a)(); writeln("Result: ", result);
Parallelism Parallel programming concepts are implemented in the library, and do not require extra support from the compiler. However the D type system and compiler ensure that data sharing can be detected and managed transparently.
import std.stdio : writeln; import std.range : iota; import std.parallelism : parallel;
void main() { foreach (i; iota(11).parallel) { // The body of the foreach loop is executed in parallel for each i writeln("processing ", i);
iota(11).parallel is equivalent to std.parallelism.parallel(iota(11)) by using UFCS.
The same module also supports taskPool which can be used for dynamic creation of parallel tasks, and for map-filter-reduce and fold style operations on ranges (and arrays), which is useful when combined with functional operations. std.algorithm.map returns a lazily evaluated range rather than an array. This way, the elements are computed by each worker task in parallel automatically.
import std.stdio : writeln; import std.algorithm : map; import std.range : iota; import std.parallelism : taskPool;
/* On Intel i7-3930X and gdc 9.3.0: * 5140ms using std.algorithm.reduce * 888ms using std.parallelism.taskPool.reduce * * On AMD Threadripper 2950X, and gdc 9.3.0: * 2864ms using std.algorithm.reduce * 95ms using std.parallelism.taskPool.reduce */ void main() { auto nums = iota(1.0, 1_000_000_000.0);
auto x = taskPool.reduce!"a + b"( 0.0, map!"1.0 / (a * a)"(nums) );
writeln("Sum: ", x); }
Concurrency Concurrency is fully implemented in the library, and it does not require support from the compiler. Alternative implementations and methodologies of writing concurrent code are possible. The use of D typing system does help ensure memory safety.
import std.stdio, std.concurrency, std.variant;
void foo() { bool cont = true;
while (cont) { // Delegates are used to match the message type. receive( (int msg) => writeln("int received: ", msg), (Tid sender) { cont = false; sender.send(-1); }, (Variant v) => writeln("huh?") // Variant matches any type );
void main() { auto tid = spawn(&foo); // spawn a new thread running foo()
foreach (i; 0 .. 10) { tid.send(i); // send some integers
tid.send(1.0f); // send a float tid.send("hello"); // send a string tid.send(thisTid); // send a struct (Tid)
receive((int x) => writeln("Main thread received message: ", x)); }
Metaprogramming Metaprogramming is supported through templates, compile-time function execution, tuples, and string mixins. The following examples demonstrate some of D's compile-time features.
Templates in D can be written in a more imperative style compared to the C++ functional style for templates. This is a regular function that calculates the factorial of a number:
ulong factorial(ulong n) { if (n < 2) { return 1; } else { return n * factorial(n-1);
Here, the use of static if, D's compile-time conditional construct, is demonstrated to construct a template that performs the same calculation using code that is similar to that of the function above:
template Factorial(ulong n) { static if (n < 2) { enum Factorial = 1; } else { enum Factorial = n * Factorial!(n-1);
In the following two examples, the template and function defined above are used to compute factorials. The types of constants need not be specified explicitly as the compiler infers their types from the right-hand sides of assignments:
enum factorial7 = Factorial!(7);
This is an example of compile-time function execution (CTFE). Ordinary functions may be used in constant, compile-time expressions provided they meet certain criteria:
enum factorial9 = factorial(9);
The std.string.format function performs printf-like data formatting (also at compile-time, through CTFE), and the "msg" pragma displays the result at compile time:
import std.string : format;
pragma(msg, format("7! = %s", fact_7)); pragma(msg, format("9! = %s", fact_9));
String mixins, combined with compile-time function execution, allow for the generation of D code using string operations at compile time. This can be used to parse domain-specific languages, which will be compiled as part of the program:
// hypothetical module which contains a function that parses Foo source code // and returns equivalent D code import FooToD;
void main() { mixin(fooToD(import("example.foo"))); }
Memory management Memory is usually managed with garbage collection, but specific objects may be finalized immediately when they go out of scope. This is what the majority of programs and libraries written in D use.
Explicit memory management is possible using the overloaded operator new, by calling C's malloc and free directly, or implementing custom allocator schemes such as stack allocation with fallback, RAII-style allocation, reference counting, or shared reference counting. Garbage collection can be controlled: programmers may add and exclude memory ranges from being observed by the collector, can disable and enable the collector and force either a generational or full collection cycle. The language documentation provides examples of how to implement alternative memory management schemes for scenarios where garbage collection is not preferred.
In functions, struct instances are by default allocated on the stack, while class instances by default allocated on the heap (with only reference to the class instance being on the stack). However this can be changed for classes, for example using standard library template std.typecons.scoped, or by using new for structs and assigning to a pointer instead of a value-based variable.