Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Difference between dlang templates and templated classes, structs and functions

Tags:

templates

d

I have some trouble understanding templates in D.

I understand what a struct Foo(T) { } or the equivalent for a class or function does, but what is a template Bar(T) { }? how does it differ from a class, struct or function template and when would I use one?

like image 642
blipman17 Avatar asked Jan 06 '18 11:01

blipman17


2 Answers

When you see template bar(T), you can think of it as a namespace - sort of like a struct or class. Just like struct Foo(T), the contents are of course templated on the template argument, and are generally only accessible through bar!T.memberName.

I say generally, because there are some special rules. First, we have eponymous templates. If template foo(T) has a member with the exact same name as the template, then foo!T is a synonym for foo!T.foo, and the whole namespace idea sorta disappears. Other members inside foo!T are hidden and inaccessible. In fact, when you write struct Foo(T) {}, the compiler turns it into template Foo(T) { struct Foo {} }.

The other special case is mixin templates, which are basically snippets that will be dropped in verbatim when you instantiate them. That is, this code (note the mixin keyword before template):

mixin template Foo() {
    int i;
}

struct Bar {
    mixin Foo!();
}

is functionally equivalent to this code:

struct Bar {
    int i;
}

Now, why would you use just template? The first is template metaprogramming. If you look at std.traits or std.meta, those modules are filled with templates. Not mixin templates, not templated structs, classes or functions, but just templates. These operate on the values and types passed to them, and returns some kind of value or type.

A very simple example of a template used like this is std.meta.Reverse. It takes a list of arguments and reverses them, and looks like this:

template Reverse(TList...)
{
    static if (TList.length <= 1)
    {
        alias Reverse = TList;
    }
    else
    {
        alias Reverse =
            AliasSeq!(
                Reverse!(TList[$/2 ..  $ ]),
                Reverse!(TList[ 0  .. $/2]));
    }
}

Another case where you'd want to use templates is if your templated type should be elided in some cases. Say you're making your own Nullable(T), and you want Nullable!(Nullable!T) to always be Nullable!T. If you just wrote struct Nullable(T) {}, you would not get this behavior, and you'd end up with double-nullable types. The solution is to use a template constraint:

struct Nullable(T) if (!isNullable!T) {}

and a template to handle the degenerate case:

template Nullable(T) if (isNullable!T) {
    alias Nullable = T;
}

I hope this was of some help, and please ask if anything's unclear. :)

like image 61
BioTronic Avatar answered Dec 30 '22 21:12

BioTronic


Well, technically,

struct S(T)
{
    ...
}

is equivalent to

template S(T)
{
    struct S
    {
        ...
    }
}

and

auto foo(T)(T t)
{
    ...
}

is equivalent to

template foo(T)
{
    auto foo(T t)
    {
        ...
    }
}

It's just that the shorter syntax is provided to make things cleaner for a common use case. template creates a template for code generation. Nothing within that template exists as real code before the template is instantiated with arguments, and the code that gets generated depends on the template arguments. So, semantic analysis for a template is not done until until it's instantiated.

Part of what's happening with structs, classes, and functions which have the template as part of their declaration instead of explicitly declaring the template to wrap them is what's called an eponymous template. Any template that has a symbol in it with the same name as the template is replaced with that symbol when the template is used. e.g.

template isInt(T)
{
    enum isInt = is(T == int);
}

could then be used in an expression such as

auto foo = isInt!int;

and the value of enum isInt.isInt is used in the expression in place of the template. This technique is used heavily with helper templates for template constraints. e.g. isInputRange in

auto foo(R)(R range)
    if(isInputRange!R)
{...}

isInputRange is defined as an eponymous template that results in true if the given type is an input range and false otherwise. In a way, it's kind of like having a function for operating on types, though it can operate on values as well, and the result doesn't have to be bool. e.g.

template count(Args...)
{
     enum count = Args.length;
}

or

template ArrayOf(T)
{
    alias ArrayOf = T[];
}

There's also a shortcut syntax for eponymous templates which aren't user-defined types or functions if they don't have any other members. e.g.

enum count(Args...) = Args.length;
alias ArrayOf(T) = T[];

And as I said, an eponymous template can be a bit like having a function to operate on a type, and that's what they're used for when complicated operations need to be done on types. For instance, using that ArrayOf template with std.meta.staticMap, you can do something like

alias Arrays = staticMap!(ArrayOf, int, float, byte, bool);
static assert(is(Arrays == AliasSeq!(int[], float[], byte[], bool[])));

This can be extremely useful in template constraints (or other eponymous templates to be used in template constraints), or it can be used with something like static foreach to more explicitly generate code. e.g. if I wanted to test some code with all string types, I could write something like

alias Arrays(T) = AliasSeq!(T[],
                            const(T)[],
                            const(T[]),
                            immutable(T)[],
                            immutable(T[]));

unittest
{
    import std.conv : to;
    static foreach(S; AliasSeq(Arrays!char, Arrays!wchar, Arrays!dchar))
    {{
        auto s = to!S("foo");
        ...
    }}
}

These techniques are typically used quite heavily in metaprogramming. They can be used on values in addition to types, but it's usually more efficient to use CTFE for values rather than putting them in an AliasSeq and using various eponymous templates on them. For instance, years ago, Phobos used to have the eponymous template Format which was used to generate strings at compile time similarly to how std.format.format does that at runtime, but once CTFE was improved to the point that format could be used at compile-time, it made no sense to use Format instead of format, because Format was horribly slow in comparison (doing a lot of recursive template instantiations can get to be expensive). So, using template metaprogramming is still required when operating on types, but if you can do what you need to do with CTFE, that's generally better.

If you're looking for metaprogramming tools in Phobos, std.traits and std.meta are the main place to look, though some are scattered throughout Phobos depending on what they're for (e.g. the range-specific ones are in std.range.primitives).

Also, similarly to how you can mixin strings, you can mixin templates. e.g.

template foo(T)
{
    T i;
}

void main()
{
    mixin foo!int;
    auto a = i;
}

So, the code that's generated by the template essentially gets copy-pasted where you mix it in. Optionally, you can put mixin on the template declaration to make it illegal to use it as anything other than a mixin. e.g.

mixin template foo(T)
{
    T i;
}

Personally, I usually just use string mixins for that sort of thing, but some folks prefer template mixins.

like image 29
Jonathan M Davis Avatar answered Dec 30 '22 20:12

Jonathan M Davis