Metaprogramming in C++ and in D

Paul Manta picture Paul Manta · Sep 4, 2011 · Viewed 11.8k times · Source

The template mechanism in C++ only accidentally became useful for template metaprogramming. On the other hand, D's was designed specifically to facilitate this. And apparently it's even easier to understand (or so I've heard).

I've no experience with D, but I'm curious, what is it that you can do in D and you cannot in C++, when it comes to template metaprogramming?

Answer

Jonathan M Davis picture Jonathan M Davis · Sep 5, 2011

The two biggest things that help template metaprogramming in D are template constraints and static if - both of which C++ could theoretically add and which would benefit it greatly.

Template constraints allow you to put a condition on a template that must be true for the template to be able to be instantiated. For instance, this is the signature of one of std.algorithm.find's overloads:

R find(alias pred = "a == b", R, E)(R haystack, E needle)
    if (isInputRange!R &&
        is(typeof(binaryFun!pred(haystack.front, needle)) : bool))

In order for this templated function to be able to be instantiated, the type R must be an input range as defined by std.range.isInputRange (so isInputRange!R must be true), and the given predicate needs to be a binary function which compiles with the given arguments and returns a type which is implicitly convertible to bool. If the result of the condition in the template constraint is false, then the template won't compile. Not only does this protect you from the nasty template errors that you get in C++ when templates won't compile with their given arguments, but it makes it so that you can overload templates based on their template constraints. For instance, there's another overload of find which is

R1 find(alias pred = "a == b", R1, R2)(R1 haystack, R2 needle)
if (isForwardRange!R1 && isForwardRange!R2
        && is(typeof(binaryFun!pred(haystack.front, needle.front)) : bool)
        && !isRandomAccessRange!R1)

It takes exactly the same arguments, but its constraint is different. So, different types work with different overloads of the same templated function, and the best implementation of find can be used for each type. There's no way to do that sort of thing cleanly in C++. With a bit of familiarity with the functions and templates used in your typical template constraint, template constraints in D are fairly easy to read, whereas you need some very complicated template metaprogramming in C++ to even attempt something like this, which your average programmer is not going to be able to understand, let alone actually do on their own. Boost is a prime example of this. It does some amazing stuff, but it's incredibly complicated.

static if improves the situation even further. Just like with template constraints, any condition which can be evaluated at compile time can be used with it. e.g.

static if(isIntegral!T)
{
    //...
}
else static if(isFloatingPoint!T)
{
    //...
}
else static if(isSomeString!T)
{
    //...
}
else static if(isDynamicArray!T)
{
    //...
}
else
{
    //...
}

Which branch is compiled in depends on which condition first evaluates to true. So, within a template, you can specialize pieces of its implementation based on the types that the template was instantiated with - or based on anything else which can be evaluated at compile time. For instance, core.time uses

static if(is(typeof(clock_gettime)))

to compile code differently based on whether the system provides clock_gettime or not (if clock_gettime is there, it uses it, otherwise it uses gettimeofday).

Probably the most stark example that I've seen where D improves on templates is with a problem which my team at work ran into in C++. We needed to instantiate a template differently based on whether the type it was given was derived from a particular base class or not. We ended up using a solution based on this stack overflow question. It works, but it's fairly complicated for just testing whether one type is derived from another.

In D, however, all you have to do is use the : operator. e.g.

auto func(T : U)(T val) {...}

If T is implicitly convertible to U (as it would be if T were derived from U), then func will compile, whereas if T isn't implicitly convertible to U, then it won't. That simple improvement makes even basic template specializations much more powerful (even without template constraints or static if).

Personally, I rarely use templates in C++ other than with containers and the occasional function in <algorithm>, because they're so much of a pain to use. They result in ugly errors and are very hard to do anything fancy with. To do anything even a little bit complicated, you need to be very skilled with templates and template metaprogramming. With templates in D though, it's so easy that I use them all the time. The errors are much easier to understand and deal with (though they're still worse than errors typically are with non-templated functions), and I don't have to figure out how to force the language into doing what I want with fancy metaprogramming.

There's no reason that C++ couldn't gain much of these abilities that D has (C++ concepts would help if they ever get those sorted out), but until they add basic conditional compilation with constructs similar to template constraints and static if to C++, C++ templates just won't be able to compare with D templates in terms of ease of use and power.