Programming in D for C++ Programmers
Every experienced C++ programmer accumulates a series of idioms and techniques which become second nature. Sometimes, when learning a new language, those idioms can be so comfortable it's hard to see how to do the equivalent in the new language. So here's a collection of common C++ techniques, and how to do the corresponding task in D.
See also: Programming in D for C ProgrammersDefining constructors
The C++ Way
Constructors have the same name as the class:class Foo { Foo(int x); };
The D Way
Constructors are defined with the this keyword:class Foo { this(int x) { } }which reflects how they are used in D.
Base class initialization
The C++ Way
Base constructors are called using the base initializer syntax.class A { A() {... } }; class B : A { B(int x) : A() // call base constructor { ... } };
The D Way
The base class constructor is called with the super syntax:class A { this() { ... } } class B : A { this(int x) { ... super(); // call base constructor ... } }It's superior to C++ in that the base constructor call can be flexibly placed anywhere in the derived constructor. D can also have one constructor call another one:
class A { int a; int b; this() { a = 7; b = foo(); } this(int x) { this(); a = x; } }Members can also be initialized to constants before the constructor is ever called, so the above example is equivalently written as:
class A { int a = 7; int b; this() { b = foo(); } this(int x) { this(); a = x; } }
Comparing structs
The C++ Way
While C++ defines struct assignment in a simple, convenient manner:struct A x, y; ... x = y;it does not for struct comparisons. Hence, to compare two struct instances for equality:
#include <string.h> struct A x, y; inline bool operator==(const A& x, const A& y) { return (memcmp(&x, &y, sizeof(struct A)) == 0); } ... if (x == y) ...
Note that the operator overload must be done for every struct needing to be compared, and the implementation of that overloaded operator is free of any language help with type checking. The C++ way has an additional problem in that just inspecting the (x == y) does not give a clue what is actually happening, you have to go and find the particular overloaded operator==() that applies to verify what it really does.
There's a nasty bug lurking in the memcmp() implementation of operator==(). The layout of a struct, due to alignment, can have ‘holes’ in it. C++ does not guarantee those holes are assigned any values, and so two different struct instances can have the same value for each member, but compare different because the holes contain different garbage.
To address this, the operator==() can be implemented to do a memberwise compare. Unfortunately, this is unreliable because (1) if a member is added to the struct definition one may forget to add it to operator==(), and (2) floating point nan values compare unequal even if their bit patterns match.
There just is no robust solution in C++.The D Way
D does it the obvious, straightforward way:A x, y;
...
if (x == y)
...
Creating a new typedef'd type
The C++ Way
Typedefs in C++ are weak, that is, they really do not introduce a new type. The compiler doesn't distinguish between a typedef and its underlying type.#define HANDLE_INIT ((Handle)(-1)) typedef void *Handle; void foo(void *); void bar(Handle); Handle h = HANDLE_INIT; foo(h); // coding bug not caught bar(h); // okThe C++ solution is to create a dummy struct whose sole purpose is to get type checking and overloading on the new type.
#define HANDLE_INIT ((void *)(-1)) struct Handle { void *ptr; // default initializer Handle() { ptr = HANDLE_INIT; } Handle(int i) { ptr = (void *)i; } // conversion to underlying type operator void*() { return ptr; } }; void bar(Handle); Handle h; bar(h); h = func(); if (h != HANDLE_INIT) ...
The D Way
No need for idiomatic constructions like the above. You can use std.typecons.Typedef:alias Handle = Typedef!(void*, cast(void*)-1); void bar(Handle); Handle h; bar(h); h = func(); if (h != Handle.init) ...Unlike a bare alias, using std.typecons.Typedef ensures the two types are not considered as equals. Note how a default initializer can be supplied to std.typecons.Typedef as a value of the underlying type.
Friends
The C++ Way
Sometimes two classes are tightly related but not by inheritance, but need to access each other's private members. This is done using friend declarations:class A { private: int a; public: int foo(B *j); friend class B; friend int abc(A *); }; class B { private: int b; public: int bar(A *j); friend class A; }; int A::foo(B *j) { return j->b; } int B::bar(A *j) { return j->a; } int abc(A *p) { return p->a; }
The D Way
In D, friend access is implicit in being a member of the same module. It makes sense that tightly related classes should be in the same module, so implicitly granting friend access to other module members solves the problem neatly:module X; class A { private: static int a; public: int foo(B j) { return j.b; } } class B { private: static int b; public: int bar(A j) { return j.a; } } int abc(A p) { return p.a; }The private attribute prevents other modules from accessing the members.
Operator overloading
The C++ Way
Given a struct that creates a new arithmetic data type, it's convenient to overload the comparison operators so it can be compared against integers:struct A { int operator < (int i); int operator <= (int i); int operator > (int i); int operator >= (int i); }; int operator < (int i, A &a) { return a > i; } int operator <= (int i, A &a) { return a >= i; } int operator > (int i, A &a) { return a < i; } int operator >= (int i, A &a) { return a <= i; }A total of 8 functions are necessary.
The D Way
D recognizes that the comparison operators are all fundamentally related to each other. So only one function is necessary:struct A { int opCmp(int i); }
The compiler automatically interprets all the <, <=, > and >= operators in terms of the cmp function, as well as handling the cases where the left operand is not an object reference.
Similar sensible rules hold for other operator overloads, making using operator overloading in D much less tedious and less error prone. Far less code needs to be written to accomplish the same effect.
Namespace using declarations
The C++ Way
A using-declaration in C++ is used to bring a name from a namespace scope into the current scope:namespace foo { int x; } using foo::x;
The D Way
D uses modules instead of namespaces and #include files, and alias declarations take the place of using declarations:/** Module foo.d **/ module foo; int x; /** Another module **/ import foo; alias x = foo.x;Alias is a much more flexible than the single purpose using declaration. Alias can be used to rename symbols, refer to template members, refer to nested class types, etc.
RAII (Resource Acquisition Is Initialization)
The C++ Way
In C++, resources like memory, etc., all need to be handled explicitly. Since destructors automatically get called when leaving a scope, RAII is implemented by putting the resource release code into the destructor:class File { Handle *h; ~File() { h->release(); } };
The D Way
The bulk of resource release problems are simply keeping track of and freeing memory. This is handled automatically in D by the garbage collector. The second common resources used are semaphores and locks, handled automatically with D's synchronized declarations and statements.
The few RAII issues left are handled by structs. A struct gets its destructor run when it goes out of scope.
struct File { Handle h; ~this() { h.release(); } } void test() { if (...) { auto f = File(); ... } // f.~this() gets run at closing brace, even if // scope was exited via a thrown exception }
classes are typically managed by the garbage collector which doesn't lend itself to RAII. If you need deterministic destruction with classes you can use std.typecons.scoped (which will also allocate the class on the stack instead of the garbage collector managed heap).
See also ScopeGuardStatement for a more generalized mechanism that lets you run arbitrary statements whenever leaving the current scope.
Properties
The C++ Way
It is common practice to define a field, along with object-oriented get and set functions for it:class Abc { public: void setProperty(int newproperty) { property = newproperty; } int getProperty() { return property; } private: int property; }; Abc a; a.setProperty(3); int x = a.getProperty();All this is quite a bit of typing, and it tends to make code unreadable by filling it with getProperty() and setProperty() calls.
The D Way
Properties can be get and set using the normal field syntax, yet the get and set will invoke methods instead.class Abc { // set @property void property(int newproperty) { myprop = newproperty; } // get @property int property() { return myprop; } private: int myprop; }which is used as:
Abc a = new Abc; a.property = 3; int x = a.property;Thus, in D a property is treated like it was a simple field name. A property can start out actually being a simple field name, but if later it becomes necessary to make getting and setting it function calls, no code needs to be modified other than the class definition. It obviates the wordy practice of defining get and set properties ‘just in case’ a derived class should need to override them. It's also a way to have interface classes, which do not have data fields, behave syntactically as if they did.
Recursive Templates
The C++ Way
An advanced use of templates is to recursively expand them, relying on specialization to end it. A template to compute a factorial would be:template<int n> class factorial { public: enum { result = n * factorial<n - 1>::result }; }; template<> class factorial<1> { public: enum { result = 1 }; }; void test() { printf("%d\n", factorial<4>::result); // prints 24 }
The D Way
The D version is analogous, though a little simpler, taking advantage of Eponymous Templates - promotion of single template members to the enclosing name space:template factorial(int n) { enum factorial = n * .factorial!(n-1); } template factorial(int n : 1) { enum factorial = 1; } void test() { writeln(factorial!(4)); // prints 24 }The template blocks can be made shorter using Enum Template syntax:
enum factorial(int n) = n * .factorial!(n-1); enum factorial(int n : 1) = 1;
Meta Templates
The problem: create a typedef for a signed integral type that is at least nbits in size.The C++ Way
This example is simplified and adapted from one written by Dr. Carlo Pescio in Template Metaprogramming: Make parameterized integers portable with this novel technique.
There is no way in C++ to do conditional compilation based on the result of an expression based on template parameters, so all control flow follows from pattern matching of the template argument against various explicit template specializations. Even worse, there is no way to do template specializations based on relationships like "less than or equal to", so the example uses a clever technique where the template is recursively expanded, incrementing the template value argument by one each time, until a specialization matches. If there is no match, the result is an unhelpful recursive compiler stack overflow or internal error, or at best a strange syntax error.
A preprocessor macro is also needed to make up for the lack of template typedefs.#include <limits.h> template< int nbits > struct Integer { typedef Integer< nbits + 1 > :: int_type int_type ; }; struct Integer< 8 > { typedef signed char int_type ; }; struct Integer< 16 > { typedef short int_type ; }; struct Integer< 32 > { typedef long int_type ; }; struct Integer< 64 > { typedef long long int_type ; }; // If the required size is not supported, the metaprogram // will increase the counter until an internal error is // signaled, or INT_MAX is reached. The INT_MAX // specialization does not define a int_type, so a // compiling error is always generated struct Integer< INT_MAX > { }; // A bit of syntactic sugar #define Integer( nbits ) Integer< nbits > :: int_type #include <stdio.h> int main() { Integer( 8 ) i ; Integer( 16 ) j ; Integer( 29 ) k ; Integer( 64 ) l ; printf("%d %d %d %d\n", sizeof(i), sizeof(j), sizeof(k), sizeof(l)); return 0 ; }
The C++ Boost Way
This version uses the C++ Boost library. It was provided by David Abrahams.#include <boost/mpl/if.hpp> #include <boost/mpl/assert.hpp> template <int nbits> struct Integer : mpl::if_c<(nbits <= 8), signed char , mpl::if_c<(nbits <= 16), short , mpl::if_c<(nbits <= 32), long , long long>::type >::type > { BOOST_MPL_ASSERT_RELATION(nbits, <=, 64); } #include <stdio.h> int main() { Integer< 8 > i ; Integer< 16 > j ; Integer< 29 > k ; Integer< 64 > l ; printf("%d %d %d %d\n", sizeof(i), sizeof(j), sizeof(k), sizeof(l)); return 0 ; }
The D Way
The D version could also be written with recursive templates, but there's a better way. Unlike the C++ example, this one is fairly easy to figure out what is going on. It compiles quickly, and gives a sensible compile time message if it fails.import std.stdio; template Integer(int nbits) { static if (nbits <= 8) alias Integer = byte; else static if (nbits <= 16) alias Integer = short; else static if (nbits <= 32) alias Integer = int; else static if (nbits <= 64) alias Integer = long; else static assert(0); } int main() { Integer!(8) i ; Integer!(16) j ; Integer!(29) k ; Integer!(64) l ; writefln("%d %d %d %d", i.sizeof, j.sizeof, k.sizeof, l.sizeof); return 0; }
Type Traits
Type traits are another term for being able to find out properties of a type at compile time.The C++ Way
The following template comes from C++ Templates: The Complete Guide, David Vandevoorde, Nicolai M. Josuttis pg. 353 which determines if the template's argument type is a function:template<typename T> class IsFunctionT { private: typedef char One; typedef struct { char a[2]; } Two; template<typename U> static One test(...); template<typename U> static Two test(U (*)[1]); public: enum { Yes = sizeof(IsFunctionT<T>::test<T>(0)) == 1 }; }; void test() { typedef int (fp)(int); assert(IsFunctionT<fp>::Yes == 1); }This template relies on the SFINAE (Substitution Failure Is Not An Error) principle. Why it works is a fairly advanced template topic.
The D Way
SFINAE (Substitution Failure Is Not An Error) can be done in D without resorting to template argument pattern matching:template IsFunctionT(T) { static if ( is(T[]) ) const int IsFunctionT = 0; else const int IsFunctionT = 1; } void test() { alias int fp(int); assert(IsFunctionT!(fp) == 1); }The task of discovering if a type is a function doesn't need a template at all, nor does it need the subterfuge of attempting to create the invalid array of functions type. The IsExpression expression can test it directly:
void test() { alias int fp(int); assert( is(fp == function) ); }
Interfaces
Interfaces in C++ are used to describe the behaviour or capabilities of any class that implements the interface, without committing to a particular implementation of the interface's declared methods.Interfaces in C++
"C++ interfaces are implemented by means of abstract classes. An abstract class defines interface methods as pure virtual functions. A class implementing the interface inherits from the abstract class and implements its pure virtual functions."class Interface { public: virtual int method() = 0; }; class FirstVariant : public Interface { public: int method() { ... } }; class SecondVariant : public Interface { public: int method() { ... } };The interface is used as follows:
FirstVariant FirstVariant; SecondVariant SecondVariant; FirstVariant.method(); SecondVariant.method();
Interfaces in D
In D, interfaces Alexandrescu 2010, p. 212; Cehreli 2017, p. 347 are implemented with the help of the interface keyword, which introduces a class containing mainly unimplemented method declarations (non-static method definitions and non-static data members are not allowed). The implementation class inherits one or more interfaces and implements the interface methods.interface Interface { int method(); } class FirstVariant : Interface { int method() { ... } } class SecondVariant : Interface { int method() { ... } }The interface is used as follows:
FirstVariant FirstVariant = new FirstVariant(); SecondVariant SecondVariant = new SecondVariant(); FirstVariant.method(); SecondVariant.method();
References in D
D doesn't have a (C++-style) concept of references as part of the type. Arguments can be passed by reference - hence the ref keyword, but "free" references don't exist in the language. The ref in foreach loop variables can be conceptually thought of as a parameter to the loop body as well. (For opApply based iteration, the loop body indeed gets turned into a function; for "plain" iteration the compiler AST internally has special ref variables, but they are not visible to the language.) In the below code, d2 is a value copy of gallery[0] and is not a reference.module test; void main() { struct Data { int id; } import std.container.array : Array; Array!Data gallery; Data d1; gallery.insertBack(d1); auto d2 = gallery[0]; d2.id = 1; assert(d2.id == gallery[0].id, "neither ref nor pointer"); }
auto d2 = &gallery[0];