Beyond the basics

A lot can be accomplished using the template features we've examined so far. In this section, we're going to see how to make our templates more powerful, through features that are easy to learn and use. We'll start with template specializations.

Template specializations

All instantiations of a template get the same implementation of the template body. Parameters may be substituted in different places, but the overall implementation doesn't change. However, there are times when it's useful for a template to behave differently when instantiated with different types. Simple cases, where one or two lines are different for one or two types, are easy to configure at compile time with static if, but sometimes the code is hard to read. In more complex cases, such as when different types require completely different implementations, static if is not practical.

Template specializations allow us to implement multiple versions of the same template for different types. Earlier, we implemented a function template called sum that takes two arguments and adds them together. Let's assume for the sake of this example that, when dealing with floating point types, we'd like to round to the nearest whole number. Such a simple case can be implemented with static if and std.traits.isFloatingPoint:

T sum(T)(T lhs, T rhs) {
  import std.traits : isFloatingPoint;
  T ret = lhs + rhs;
  static if(isFloatingPoint!T) {
    import std.math : round;
    ret = round(ret);
  }
  return ret;
}

Easily done, but look at the tradeoff; the simple one-line function body is now seven lines long. Also, there's a new dependency on std.traits just to determine whether we're dealing with a floating point type. We can do better. A template can be made to specialize on a type by declaring the type identifier as normal, followed by a colon and the name of the specialized type. The following version of sum specializes on all floating point types:

T sum(T : real)(T lhs, T rhs) {
  import std.math : round;
  return round(lhs + rhs);
}

Since all floating point types are implicitly convertible to real, this will catch them all. Unfortunately, this won't quite do the trick by itself:

T sum(T)(T lhs, T rhs) {
  return cast(T)(lhs + rhs);
}
T sum(T : real)(T lhs, T rhs) {
  import std.math : round;
  return round(lhs + rhs);
}
void main() {
  import std.stdio : writeln;
  writeln(sum(10, 20));
}

Save this as $LEARNINGD/Chapter05/sum.d. Attempting to compile should produce the following compiler errors:

sum.d(9): Error: cannot implicitly convert expression (round(cast(real)(lhs + rhs))) of type real to int
sum.d(12): Error: template instance sum.sum!int error instantiating

The second error indicates that the template failed to instantiate. The first error, which comes from inside the template body, shows the reason instantiation failed. Recall that integrals are implicitly convertible to floating point types, but the reverse is not true. In the example, sum is instantiated with int, since that's the type of both 10 and 20. This matches the specialization because int is implicitly convertible to real. The error happens because the instantiation of sum expects to return int, but it's actually trying to return real, which is the return type of round. Since real is not implicitly convertible to int, the compiler errors out and the template fails to instantiate. To fix this, add a new specialization to catch only integrals.

Since all integrals are implicitly convertible to ulong, and since it's a better match for integrals than real, it can be used to get the job done. With two specializations to catch floating point and integral types, the original will pick up anything remaining, such as arrays, pointers or user-defined types. To disallow those, simply delete the original template and keep the two specializations:

import std.stdio;
T sum(T : ulong)(T lhs, T rhs) {
  writeln("Integral specialization.");
  return cast(T)(lhs + rhs);
}
T sum(T : real)(T lhs, T rhs) {
  writeln("Floating-point specialization.");
  import std.math : round;
  return round(lhs + rhs);
}
void main() {
  writeln(sum(10, 20));
  writeln(sum(10.11, 3.22));
}

Specialization on pointers and arrays

When sum is instantiated, it isn't necessary to explicitly specify a type; the type is implicitly deduced from the function arguments. Unfortunately, when a type is implicitly deduced, it's possible that no specialization will match. In practice, this isn't a problem for most types. While it works just fine for the sum template with integrals and floating point types, you would be in for a surprise if you tried to specialize on a pointer or array.

Let's leave sum behind and implement a new function template called printVal. This will take a single argument of any type and print it to standard output. The base form of the template looks like this:

void printVal(T)(T t) {
  writeln(t);
}

Try to instantiate this with any type and it will work flawlessly. The only potential issue is what to do about pointers. By default, writeln prints the address of a pointer. If that's the desired behavior, then nothing further need be done. What if, instead, we want to print the value the pointer is pointing to? In that case, we need a specialization. Specializing on a specific type of pointer is no different than doing so for any type:

void printVal(T : int*)(T t) {
  writeln(*t);
}

But who wants to write a version of printVal for every conceivable pointer type? To specialize on any pointer, no matter the base type, the following syntax is used:

void printVal(T : U*, U)(T t) {
  writeln(*t);
}

The second template parameter, U, is what allows this function to specialize on any pointer. Whatever type U is, then T is specialized on a pointer to that type. Explicit instantiation can look like either of the following lines:

printVal!(int*, int)(&x);
printVal!(int*)(&x);

When there are multiple template parameters, it's not necessary to pass an argument for all of them if the remainder can be deduced. In the second declaration, the compiler can deduce that, if T is int*, then U must be int. This can be verified by adding writeln(U.stringof) to printVal. IFTI also works:

printVal(&x);

The same form can be used to specialize on arrays:

void printVal(T: U[], U)(T t) {
  foreach(e; t)
    writeln(e);
}
void main() {
  printVal([10, 20, 30]);
}

Note

Note that, as I write, the documentation says that IFTI will not work with templates that use type specialization. That came as a surprise to several users involved in a forum discussion, since it's been working in the compiler for quite a while. Given the history of D development, it is more likely that the documentation will be changed to match the behavior than the other way around, but the possibility does exist that the behavior could change at some point. Anyway, for now it works and code in the wild is using it.

Template constraints

Template specialization is useful, but it can be hard sometimes be difficult to get right and doesn't fit every use case. Template constraints offer a more comprehensive alternative. The following two implementations of sum achieve the same result as the two specializations from earlier:

import std.traits;
T sum(T)(T lhs, T rhs) if(isFloatingPoint!T) {
  import std.math : round;
  return round(lhs + rhs);
}
T sum(T)(T lhs, T rhs) if(isIntegral!T) {
  writeln("Integral");
  return cast(T)(lhs + rhs);
}

These can be instantiated using implicit type deduction:

writeln(sum(10,20));
writeln(sum(22.11,22.22));

A template constraint is an if statement where the condition is any expression that can be evaluated at compile time. When the compiler finds a potential match for a set of template arguments, then the condition must evaluate to true in order for the match to succeed. Otherwise, the match fails and the compiler will try to find another.

The conditions in this example are isFloatingPoint and isIntegral, both of which are templates found in std.traits. Using this approach, there's no ambiguity; an int can only match the template with the isIntegral condition. These are essentially shortcuts for what would be, if implemented manually, a string of is expressions. For example, a test for floating point would look like: if(is(T == float) || is(T == double) || is(T == real)). Imagine doing the same for all the integral types.

The constraints could be rewritten to if(is(T : real)) and if(is(T : ulong)), to test whether T is implicitly convertible to real or ulong. On the surface, this looks similar to template specialization. However, there's a big difference in the result. When a type matches more than one specialization, the one that is more specialized wins and becomes the match. Conversely, when constraints are matched on more than one template, a compiler error is produced instead; constraint matching is more precise.

Template constraints aren't just an alternative to specialization. They are also a means of limiting a template to specific instantiations. Consider the case where it's desirable to define a function interface intended to be used among a number of different class and struct types. If it were only restricted to classes, then each class could extend and implement an actual interface, but that's not possible with structs in D. Constraints can be used to make sure that any class or struct instance provides a specific interface.

As a simple example, imagine a function called printOut that is defined to take no parameters, to return void, and to print a class or struct instance to standard output. In any given template, we want to know that it's actually possible to call printOut on an instance, either as a member function or via UFCS. Since this is something that is likely to be repeated in multiple templates, it will be useful to implement an enum template that checks whether printOut exists on any given type. A good name for it would be hasPrintOut. It might look like this:

enum hasPrintOut(T) = is(typeof(T.printOut));

We saw the is(typeof()) idiom in the previous chapter. Here, we're only checking whether the given type has a member function named printOut, but not whether the return type matches that of the expected interface. For a simple example like this, that doesn't matter. Now, with hasPrintOut in hand, a constraint can be implemented on any template that wants to call printOut on any type. For example:

void print(T)(T t) if(hasPrintOut!T) {
  t.printOut();
}

Template constraints can be as simple or as complex as they need to be. A proper implementation of hasPrintOut would verify that the return type and function parameter list match the interface. Such complex constraints, or those used often, should generally be wrapped up in a separate template such as hasPrintOut or isFloatingPoint to keep the declaration clean and readable.

Template mixins

Earlier, we saw that template instantiations have the scope of the declaration, not the instantiation. Template mixins turn that upside down. A template mixin is a special kind of template that can essentially be copied and pasted into a different scope. On the surface, they appear to be identical to the string mixins we saw in Chapter 4, Running Code at Compile Time. Digging a little deeper shows they aren't quite the same:

mixin template Mixalot() {
  int count = 10;
  int increase(int x) {
    return x + count;
  }
}

First up, note that the declaration is a regular template declaration with the mixin keyword in front. It can have any number of valid declarations in the body, excluding module declarations. Although this particular mixin has no parameters, they can be parameterized like other templates. To instantiate a mixin, we again use the mixin keyword. When the mixin has no parameters, the instantiation operator and parentheses can be elided, as in the following example:

int count = 100;
mixin Mixalot;
writeln(increase(20));
writeln(count);

Here, the declarations inside Mixalot are inserted directly into the current context. increase can be called without the need to prefix it with a Mixalot namespace. However, compile and run this and you'll find that the first writeln prints 30, not 120, and the second prints 100 instead of 10. As mixins have their own scope, increase sees the count declared inside Mixalot, not the one declared in main. Inside main, the local count declaration overrides the one in Mixalot. Let's see what happens if we change Mixalot to be a string mixin:

enum Mixalot = q{
int count = 10;
int increase(int x) {
  return x + count;
}
};
void main() {
  import std.stdio : writeln;
  int count = 100;
  mixin(Mixalot);
  writeln(increase(20));
  writeln(count);
}

This yields an error to the effect that main.count is already defined. String mixins don't have their own scope.

When a template mixin is parameterized, it must be instantiated with the instantiation operator. The following example does so, while demonstrating a common use case of template mixins: implementing a common interface among different struct declarations:

mixin template ValueImpl(T) {
  private T _value;
  T value() {
    return _value;
  }
  void value(T val) {
    static bool isSet;
    if(!isSet) {
      _value = val;
      isSet = true;
    }
  }
}
struct Value {
  mixin ValueImpl!int;
}
struct ExtendedValue {
  mixin ValueImpl!int;
  float extendedValue;
}
void printValue(T)(T t) if(is(typeof(T.value))) {
  import std.stdio : writeln;
  writeln(t.value);
}
void main() {
  Value val;
  val.value = 20;
  printValue(val);
  ExtendedValue exval;
  exval.value = 100;
  printValue(exval);
}

Variadic templates

A variadic template is one that accepts any number of template parameters as types, expressions, or symbols, from none to many. The following is an example:

void printArgs(T...)() if(T.length != 0) {
  foreach(sym; T)
    writeln(sym.stringof);
}

T... is a compile-time list of arguments generated from any instantiation of the template. T is the alias used to refer to the list inside the template body. Some prefer to use Args... instead, but any legal identifier is allowed. There are no function parameters here, as this particular function doesn't need them. The template argument list can be manipulated much like an array; it can be indexed and sliced, and the .length can be read (though not written to). Again, this all happens at compile time. The template constraint in this example ensures that the template cannot be instantiated with an empty template argument list, meaning printArgs!() will fail to compile.

The body of printArgs consists of a foreach loop that iterates every item in the argument list. A special property of foreach is that it can iterate a compile-time argument list like this at compile time. This means that the loop is unrolled; code is generated for the loop body for each item in the list. In this case, the only thing generated per iteration is a single call to writeln. Here's some code to clarify this:

printArgs!(int, "Pizza!", std.stdio, writeln);

When the compiler encounters this line, the template is instantiated with a compile-time argument list that consists of a type, a compile-time value, and two symbols (a module name and a function name). The result of the loop in the function body will be to generate the equivalent of the following (the output of each is shown in comments):

writeln(int.stringof);          // int
writeln("Pizza".stringof);      // "Pizza"
writeln((std.stdio).stringof);  // module stdio
writeln(writeln.stringof);      // writeln()

This is what is executed at runtime. If the writeln calls were replaced with a msg pragma, then the template would have no runtime component at all.

T... is only visible inside the template. However, it's possible to get a handle to an argument list outside an eponymous alias template:

template ArgList(T...) {
  alias ArgList = T;
}

Alternatively, the short form:

alias ArgList(T...) = T;

With this, it's possible to generate a compile-time list of types, expressions, or symbols that can be used in different ways, some of which we'll see shortly. For now:

printArgs!(ArgList!(int, string, double));

This results in the same output as if int, string, and double had been passed directly to printArgs. Any function that accepts T... can accept the result of an alias template. It's not necessary to implement your own generic alias template, however, as the std.meta module in Phobos provides one in the form of AliasSeq. The Seq part means sequence.

That's the basic functionality of variadic templates. There's more to cover in order to fully understand their power, but before we dive into more usage we first have to take an unfortunate, but necessary, detour to discuss terminology.

Terminology

If you visit http://dlang.org/template.html#TemplateTupleParameter, you'll find that the documentation refers to T... as a template tuple parameter. Dig around some more and you may find other references in the documentation, tutorials, forums, and blog posts to the terms type tuple and expression tuple (or value tuple). AliasSeq!(int, string, double) produces a type tuple, as all of the arguments are types; AliasSeq!(42, "Pizza!", 3.14) results in an expression tuple, where all of the members are expressions (values). T... can also be a list of symbols, but it's rare to refer to a symbol tuple. When the members are mixed, as in printArgs!(int, "Pizza!", std.stdio), there is no special name.

There is an obvious discrepancy in the name AliasSeq and the term tuple. There is some history here that is still playing out as I write. Shortly after I wrote the first draft of this chapter, there was a debate in the forums regarding the naming of std.typetuple.TypeTuple. The debate resulted in the name change of both the module and the template, to std.meta.AliasSeq. The discussion arose in the first place because there has always been some confusion around the use of tuple in D.

Some readers may be familiar with tuples from other languages, where they are often used as a runtime list that can hold multiple values of differing types. Phobos provides such a construct in the form of std.typecons.Tuple, instances of which can be created with the convenience function std.typecons.tuple (note the lowercase t):

auto tup1 = tuple!(int, string, double)(42, "Pizza", 3.14);
auto tup2 = tuple!("name", "x", "y")("Position", 3.0f, 2.0f);

The first version creates a Tuple with three members. The types of the members are specified as template arguments and the optional initial values as function arguments. Each member can be accessed with the index operator, so tup1[0] is 42. In the second form, the template arguments give names to each member and the types are deduced from the function arguments. This produces a Tuple instance on which tup2[1] and tup2.x both return 3.0f.

Tuple is implemented as a struct and is useful for returning multiple values from a function, or in any place where multiple values of different types need to be packaged together. Talking about tuples in D, though, has always been a bit problematic. Does it mean std.typecons.Tuple or T...? Another issue has been that TypeTuple could create not only type tuples, but expression tuples, symbol tuples, and any possible compile-time tuple. The name certainly wasn't conducive to easy understanding.

Now that we have std.meta.AliasSeq to create compile-time tuples and std.typecons.Tuple to create runtime tuples, that doesn't mean all is well quite yet. People still refer to T... as a tuple, and sometimes as a compile-time argument list, a sequence, and now even an AliasSeq. In Chapter 4, Running Code at Compile Time, I explicitly avoided the use of tuple when talking about __traits, even though the documentation for some of the traits declares the return value to be a tuple. For example allMembers returns a tuple of string literals. Then there's the use of Template Tuple Parameters in the template documentation, and the .tupleof property of structs (which we'll look at soon).

So we have a situation where a movement has begun to make tuples less confusing, but has only just gotten started. As I write, no one has yet agreed on how exactly to refer to T... in a discussion, but I must choose a term to use in this book. It is certainly now discouraged to use tuple in this context, but any other term I select may be made obsolete if the community eventually settles on a different term. Given that the documentation still uses tuple in many places and that the .tupleof struct property still exists, I will use tuple for the remainder of this discussion to refer to T... or any compile-time argument list, such as that returned by the allMembers trait. I have the benefit of formatting, so tuple and Tuple will refer to the symbols in std.typecons, while tuple will refer to the compile-time version. Keep yourself up to date by reading the latest documentation and following the forum threads to know how to map the term tuple used here to the term being used at the time you read the book. Now, back to the usage of variadic templates and tuple parameters.

More on usage

Instances of a type tuple (no expressions or symbols allowed in this case) can be declared using an alias template as the type, or an alias to the template:

import std.meta : AliasSeq;
AliasSeq!(int, string, double) isdTuple1;
alias ISD = AliasSeq!(int, string, double);
ISD isdTuple2;

Instances are runtime constructs, which are given special field names for the members. As implemented, printArgs will not print the field names of a tuple instance. Instead, it will print sym, the name of the alias in the foreach, for each member. No problem. Let's make a new function called printTuple:

void printTuple(T...)() if(T.length != 0) {
  foreach(i, _; T)
    writeln(T[i].stringof);
}

The point of this loop is to avoid using the alias, so _ is used as a visible indication that it won't be used. Instead, the current index is used to access tuple items directly. This will properly print the field names. printTuple!isdTuple1 produces this:

__isdTuple1_field_0
__isdTuple1_field_1
__isdTuple1_field_2

This explicitly shows the difference between a tuple instance and the compile-time entity; printTuple!ISD will print the type names. Moreover, the instance will have been initialized with the .init values of each type in the tuple. This can be verified with the following runtime loop:

foreach(item; isdTuple1)
  writeln(item);

It prints:

0

nan

The default initializer for a string is the empty string, so nothing is printed in the second line. Values can be assigned to the instance just like any other variable:

isdTuple1[0] = 42;
isdTuple1[1] = "Pizza!";
isdTuple1[2] = 3.14;

Tuple instances can also be expanded, or unpacked, anywhere a comma-separated list of values is expected. Consider this function:

void printThreeValues(int a, string b, double c) {
  writefln("%s, %s, %s", a, b, c);
}

Given that isdTuple1 and isdTuple2 each have three members that match the types of the function arguments (in the same order), either can be passed to the function like so:

printThreeValues(isdTuple1);

Structs have the property .tupleof, which can be used to convert all of a struct's members to a tuple. It can then also then be unpacked:

struct UnpackMe {
  int meaningOfLife;
  string meaningOfFood;
  double lifeOf;
}

Instead of passing each member individually to printThreeValues:

auto um = UnpackMe(42, "Pizza!", 3.14);
printThreeValues(um.tupleof);

Unpacking also works for compile-time tuples:

void printThreeTemplateParams(T, string U, alias V)() {
  writefln("%s, %s, %s", T.stringof, U, V.stringof);
}

The following tuple matches the parameter list:

printThreeTemplateParams!(AliasSeq!(int, "Hello", std.stdio));

Instances can also be declared as function parameters:

void printTuple2(T...)(T args) {
  foreach(i, _; T)
    pragma(msg, T[i].stringof);
  foreach(item; args)
    writeln(item);
}

This can be called like a normal function and the members of T... will be deduced:

printTuple2(42, "Pizza!", 3.14);

As you can see, there are a number of different ways to use tuples. The instances described in this section seem quite similar to the std.typecons.Tuple type mentioned earlier, but they have very different implementations. At this point, I hope you understand why there is a movement afoot to change the tuple terminology.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.129.253