Chapter 5. Generic Programming Made Easy

One of the benefits of generic programming is that it enables the implementation of type-independent code. A single function can be written once to support multiple types, rather than once for each supported type. Several languages allow for generic programming to one degree or another. Some implementations are easy to use, but not very powerful; others are powerful, but difficult to learn. Throughout my time in the D community, I have seen numerous remarks in the newsgroups, reddit threads, and elsewhere, praising the simplicity and power of D templates. Combined with the compile-time features covered in the previous chapter, even novice programmers can quickly learn to do things that might seem daunting in other languages.

I have to work from the assumption that many readers will not be as familiar with generic programming as others. With that in mind, we're going to start with a look at the very basics of using templates in D and progressively work our way through to more advanced usage. We aren't going to cover everything there is to know about templates, but we'll cover enough that you'll be able to use them to great effect in your own code. The flow of the chapter looks like this:

  • Template declarations: templates as code blocks, struct, class, enum, and function templates
  • Template parameters: value, alias, and this parameters
  • Beyond the basics: template specializations, template constraints, template mixins, and variadic templates
  • Operator overloads: several overloadable operators
  • MovieMan: the database

Template basics

As a barebones definition, we might say that a template is a block of code that doesn't exist until it is used. A template can be declared in a source module, but if it is never instantiated, it doesn't get compiled into the final binary. Further, there are different ways to declare a template and several ways to control how it is compiled into the binary. In this section, we're going to explore the former.

Templates as code blocks

A template declaration looks somewhat like a function declaration. It opens with the template keyword, followed by an identifier, a parameter list, and then a pair of braces for the body. The body may contain any valid D declaration except module declarations, as the following example demonstrates:

template MyTemplate(T) {
  T val;
  void printVal() {
    import std.stdio : writeln;
    writeln("The type is ", typeid(T));
    writeln("The value is ", val);
  }
}

The first line declares a template named MyTemplate that takes one parameter, T. This isn't the same as a function parameter. There are different kinds of template parameters, but in this case T is intended to refer to a type. It can be any type: int, float, a user-defined type, and so on. Most templates are parameterized.

After the template parameter list, multiple declarations can appear inside a pair of braces. This example declares a variable of type T named val and a function called printVal that uses val. If compiled at this point, neither val nor printVal would be present in the binary. For that to happen, the template must be instantiated at least once.

When a template is instantiated, any declarations inside it are compiled, with the given template arguments replacing its parameters. The following snippet instantiates two instances of MyTemplate using two different types and two different approaches:

MyTemplate!(int).val = 20;
MyTemplate!int.printVal();
alias mtf = MyTemplate!float;
mtf.printVal();

Taken together, the two snippets yield the following output:

The type is int
The value is 20
The type is float
The value is nan

The first line of main instantiates MyTemplate with int. This is accomplished by putting the template instantiation operator, !, after the template name, followed by the type argument list. In the same line, val is set to 20. MyTemplate!(int) acts as a namespace for each declaration in the body. Both members are accessed with the dot operator.

The second line demonstrates two points. First, the parentheses around the type parameter have been dropped. When a template declaration takes only one parameter, the parentheses are usually optional in the instantiation, though sometimes they are required; for example, the brackets in an array type such as int[] make the parentheses mandatory. Second, when printVal is called, it shows that the type of T is int and its value is 20. This verifies that it refers to the same instance of the template that was instantiated in the first line, where val is set to 20. If this were part of a larger program, then any usage of MyTemplate!int, in any module, is referring to the same instance of val and the same implementation of printVal.

The third line sets up for a different approach to instantiation by creating an alias. This both instantiates the template and makes mtf a synonym for MyTemplate!float. The very next line calls printVal through the alias. This prints the type as float and the value as nan, since val was never set for the float version of MyTemplate.

Template instantiation happens in the same scope as the declaration, not that of the instantiation. Consider this module, declscope, with its addTwo function:

module declscope;
int addTwo(int x) {
  return x + 2;
}
template NumTemplate() {
  enum constant = addTwo(10);
}

It should be obvious that addTwo(10) is calling the function declared here, but what happens when NumTemplate is instantiated in the following intscope module, which has its own addTwo?:

module intscope;
int addTwo(int x) {
  // We lied, we're adding 20
  return x + 20;
}
void main() {
  import declscope, std.stdio;
  writeln(NumTemplate!().constant);
}

It's easy to believe that the body of the template is being pasted somewhere around the instantiation, but that isn't the case. Compiling this and running it results in 12, not 20, meaning declscope.addTwo is being called inside the template. Remember, a template is only instantiated once for each set of arguments and the same instantiation can be repeated in multiple modules throughout a program. If each instantiation were scoped locally, the template would no longer work as expected.

It's also worth noting that the template in the previous example takes no parameters. When it's instantiated, the instantiation operator and parentheses are required and the parentheses have to be empty. It may appear to be pointless to have a typeless template; after all, supporting multiple types with a single block of code is a major benefit of generic programming. We'll see later that typeless templates can be put to good use.

If one instance of val per instantiation is not sufficient, the template body can be rewritten so that val and printVal are wrapped inside a struct or class:

template MyTemplate(T) {
  struct ValWrapper {
    T val;
    void printVal() {
      import std.stdio : writeln;
      writeln("The type is ", typeid(T));
      writeln("The value is ", val);
    }
  }
}
void main() {
  MyTemplate!int.ValWrapper vw1;
  MyTemplate!int.ValWrapper vw2;
  vw1.val = 20;
  vw2.val = 30;
  vw1.printVal();
  vw2.printVal();
}

Although it looks like MyTemplate is instantiated twice here, that's not what's happening. The template is still instantiated only once. Instead, two instances of MyTemplate!int.ValWrapper are declared. The instantiation effectively creates a new ValWrapper declaration as if the following had been explicitly declared:

struct ValWrapper {
  int val;
  void printVal() {...}
}

If the template is instantiated with a different type, it creates a new declaration of ValWrapper. If it's never instantiated, then ValWrapper never exists as a type. Although multiple declarations can go inside a template body, it's quite common to declare a single struct, class, function, or even a manifest constant. In that case, we can do away with the tediousness of the dot operator and take some shortcuts.

Struct and class templates

In 1975, Fleetwood Mac released an album titled Fleetwood Mac. Six years later, it was in reference to that album that the 10-year-old me first made the connection between the words eponymous and self-titled while listening to the radio. In D, self-titled templates are a thing. Let's rewrite MyTemplate once more, this time making it eponymous:

template ValWrapper(T) {
  struct ValWrapper {
    T val;
    void printVal() {
      writeln("The type is ", typeid(T));
      writeln("The value is ", val);
    }
  }
}

Now that the template and struct declarations have the same name, the dot operator can be dropped from instantiations and instances of ValWrapper declared directly:

ValWrapper!int vw;

That's much nicer syntax, isn't it? The language also allows for a shortcut in the template declaration. By simply adding the parameter list to the struct declaration, the template block is eliminated completely. The declaration then becomes:

struct ValWrapper(T) {
  T val;
  void printVal() {
    writeln("The type is ", typeid(T));
    writeln("The value is ", val);
  }
}

This is a struct template. The instantiation syntax is the same as it was for the first version of the eponymous template. If it is expected to be used often, alias declarations can be added at module scope to help make the instantiation syntax even cleaner:

alias ValWrapperI = ValWrapper!int;
alias ValWrapperF = ValWrapper!float;

A class template is similar:

class ValClass(T) {
private:
  T _val;
public:
  this(T val) {
    _val = val;
  }
  T val() @property {
    return _val;
  }
}

It can be instantiated like this:

auto vc = new ValClass!int(10);

When multiple type parameters are involved, the parentheses in the instantiation are no longer optional and two pairs are needed when invoking the constructor. The following partial implementation of a wrapper for associative arrays demonstrates:

class HashMap(K,V) {
  V[K] _map;
  string _name;
  this(string name) {
    _name = name;
  }
}

When instantiating one of these, it's going to look a bit more cluttered:

auto map = new HashMap!(string, int)("NameMap");

Always remember that the first pair of parentheses contains the template arguments and the second the constructor arguments.

It's also possible to inherit from a template class or interface:

interface Transformation(T) {
  T transform(T t);
}
class Double(T) : Transformation!T {
  T transform(T t) {
    return t * 2;
  }
}

When an instance of Double is instantiated, it in turn instantiates the Transformation interface with the same type. Then, a Double!int can be passed anywhere a Transformation!int is expected:

struct Value(T) {
  T val;
  Transformation!T transformation;
  T transform() {
    val = transformation.transform(val);
    return val;
  }
}
void main() {
  import std.stdio : writeln;
  auto = Value!int(10, new Double!int);
  writeln(intVal.transform());
}

When Value is instantiated with int, its member transformation is expected to be of type Transformation!int, which is exactly what it is initialized with when new Double!int is used in the struct literal. Note that, since Value is a template, the literal form must also be a template instantiation. Though not covered here, it's also possible to declare union templates.

Enum templates

An enum template is a templated manifest constant. Let's look at the long form first:

template isLongOrInt(T) {
  enum isLongOrInt = is(T == long) || is(T == int);
}

Dig around the source for Phobos and you'll find several declarations like this, all written before the shortened syntax was introduced for enum templates. Now we can do this:

enum isLongOrInt(T) = is(T == long) || is(T == int);

Instantiating an enum template causes the value of the manifest constant in the template body to be substituted at the point it is used at:

writeln(isLongOrInt!long);
writeln(isLongOrInt!float);

In this snippet, the first instantiation will be replaced at compile time by true and the second by false. It's conceptually the equivalent of the following:

enum isLongOrInt_Long = true;
enum isLongOrInt_Float = false;
writeln(isLongOrInt_Long);
writeln(isLongOrInt_Float);

These really come in handy when working with repetitive static if conditions or, as we'll see later in this chapter, template constraints. When a compile-time condition needs frequent use, consider turning it into an enum template.

Function templates

We've already made use of a few function templates in the book, such as std.conv.to and, believe it or not, std.stdio.writeln. Before we look into why the former requires the template instantiation operator and the latter doesn't, let's first take a look at what a function template declaration looks like. First, the long form:

template sum(T) {
  T sum(T lhs, T rhs) {
    return lhs + rhs;
  }
}

And now, the more common short form:

T sum(T)(T lhs, T rhs) {
  return lhs + rhs;
}

There are two pairs of parentheses in the declaration. As is obvious in the long form, the first pair holds the template parameters and the second is for the function parameters. Instantiating and calling a function template can be done in two ways, as seen here:

auto doubles = sum!double(2.0, 3.0);
auto floats = sum(2.0f, 3.0f);
writeln(typeid(floats));

The first line instantiates the template in the same way we've seen for every case we've examined so far, by specifying the types in the argument list. Again, the parentheses are optional on a single argument, but are required if there are more. The second line is more interesting in this example. Notice that there is neither an instantiation operator nor a template argument anywhere to be found (the same is true for writeln). This is because the compiler is able to infer T from the function arguments, so there's no need to specify them. This is called Implicit Function Template Instantiation (IFTI). IFTI is quite convenient, but isn't always possible. Consider this example that wraps std.conv.to in order to convert a struct member variable into a different type:

struct Value {
  private int _val;
  T getAs(T)() {
    import std.conv : to;
    return to!T(val);
  }
}

First, note that the member function getAs is a template, but Value itself is not. Member function templates are instantiated like any other function template, except that they must be called through the dot operator on the type instance like a normal member function:

auto value = Value(100);
auto valstr = value.getAs!string();

Take out the !string bit and there is no way for the compiler to know that value.val should be converted to a string and not a double, a bool, or anything else. In that case, IFTI will fail with a compiler error. Modify getAs to take an argument to use as a default value, then the situation changes:

T getAs(T)(T defVal) {
  import std.conv : to;
  try {
    return to!T(val);
  } catch(Exception e) {
    return defVal;
  }
}

Now the compiler has enough information to implicitly deduce the type of T from the type of defVal in the function call, for example value.getAs("DefaultVal").

Tip

Reducing dependencies

In the getAs example, std.conv : to is a local import. As it is inside a template, std.conv will never be imported if the template is never instantiated. Keeping imports local when writing templated code is a great way to reduce dependencies and good practice even in non-templated code.

Special features

There are a couple of special features of function templates that are not available to normal functions. First, consider the following:

int addTwo()(int x) {
  return x + 2;
}
int addTwoInt(int x) nothrow {
  return addTwo(x);
}

Recall that a function marked nothrow can only call other functions marked nothrow. There is no guarantee that the compiler will always have the source available for a normal function, but the source for a template must always be available. Due to this, the compiler can safely use the source of any function template to infer certain function attributes (@safe, pure, nothrow, and @nogc). In this case, when addTwoInt calls addTwo, the compiler verifies that addTwo can't throw anything and allows compilation. If addTwo were to directly throw an Exception or call a function that isn't nothrow, then it would no longer be inferred as nothrow itself.

Another feature of function templates is auto ref parameters. Consider this:

void printLargeStruct (const(LargeStruct) p) {…}
void printLargeStruct(ref const(LargeStruct) p) {…}

If the first version of printLargeStruct were the only one, it would accept both l-values and r-values. The l-values would be copied, something that's inefficient for a large struct. By also declaring a ref version of the function, we ensure that l-values will be passed by reference. However, maintaining two versions of the same function is error-prone. With a function template that takes auto ref parameters, one implementation can handle both:

void printLargeStruct()(auto ref const(LargeStruct) p) {…}

Note that both addTwo and printLargetStruct have empty template parameter lists. Function templates with empty parameter lists are sometimes used in place of normal functions solely to get the benefits of auto ref.

One last thing to say about function templates: they cannot be virtual. All member functions in a class declaration are virtual by default and can be overridden by subclasses, but templated member functions cannot be.

More template parameters

While types are perhaps the most common form of template parameter, there are others. We're going to examine three of them, beginning with value parameters.

Value parameters

The following example is a partial implementation of a wrapper for D's array type:

struct Array(T, size_t size = 0) {
  static if(size > 0)
    T[size] elements;
  else
    T[] elements;
  enum isDynamic = Size == 0;
}

Array is a struct template that has two parameters, a type parameter T and a value parameter size. We know that T is a type parameter because it's a single identifier. Whether it's called T, or Type, or Foo, or whatever, a solitary identifier in a template parameter list represents a type. size is identifiable as a value parameter because it is composed of a specific type followed by an identifier, just as if it were in the parameter list of a function. A value parameter binds to any expression that can be evaluated at compile time, such as literals and function calls. Notice that size is assigned a default value. This means an argument corresponding to size is optional during instantiation, meaning Array can be instantiated with one argument for T.

Template parameters are always compile-time entities. The implementation of Array makes use of that fact to decide whether it should be a static or a dynamic array. This is accomplished at compile time with a static if. If size is greater than 0, the member variable elements is declared as a static array with length size; otherwise, elements is a dynamic array. The manifest constant isDynamic is initialized as a Boolean with size == 0, causing any read of its value to be replaced by the compiler with true or false directly.

Here are two possible instantiations of Array:

Array!int arr1;
assert(arr1.isDynamic);
Array!(float, 10) arr2;
assert(!arr2.isDynamic);

As the asserts verify, the first instantiation yields an instance of Array wrapping a dynamic array. Since size is an optional argument, it's still possible to drop the parentheses when only the first argument is specified in the instantiation. The second instantiation results in an instance containing a static array of type float and length 10. Though the example uses a literal to specify size, any compile-time expression that results in a size_t can be used. For example, given this function:

double getADouble() { return 100.0; }

We can use CTFE to instantiate the template like so:

Array!(float, cast(size_t)getADouble()) arr3;

Alias parameters

While types and values as template parameters open the door to a variety of possibilities, D goes further and allows the use of symbols as template parameters. This is possible through the use of alias parameters.

The following function template takes any symbol and prints its string representation to standard output:

void printSymbol(alias Name)() {
  writeln(Name.stringof);
}

Here the template is instantiated with several different symbols:

int x;
printSymbol!x();               // Variable name
printSymbol!printSymbol();     // Function template name
printSymbol!(std.conv.to)();   // FQN of function template
printSymbol!(std.stdio)();     // Module name

Note that the parentheses around the solitary template arguments are required for the last two instantiations due to the dots in the symbol names. The output looks like this:

x
printSymbol(alias Name)()
to(T)
module stdio

In addition to symbols, alias parameters can also bind to any expression that can be evaluated at compile time, including literals:

enum number = 10;
printSymbol!number();
printSymbol!(1+3)();
printSymbol!"Hello"();
printSymbol!(addTwo(3))();

Together with the following function:

int addTwo(int x) { return x + 2; }

This yields the following output:

10
4
"Hello"
5

As I write, D does not support any of the keyword types as template alias parameters, so instantiations such as printSymbol!int and printSymbol!class will not compile.

This parameters

Recall that every class instance has a reference to itself called this. The following class declaration includes a function that prints the type of this:

class Base {
  void printType() { writeln(typeid(this)); }
}
class Derived : Base {}

Now let's see what it prints in two specific circumstances:

Derived deri = new Derived;
Base base = new Derived;
deri.printType();
base.printType();

Running this will show that, in both cases, the printed type is Derived, which is the actual type of both instances. Most of the time, this is exactly the desired behavior, but now and again it might be useful to know the static (or declared) type of an instance, rather than the actual type. The static type of base is Base, as that is the type used in the declaration. A this parameter can be used to get the static type. These are special in that they can only be used with member functions. Change the declaration of Base to this:

class Base {
  void printType(this T)() { writeln(typeid(T)); }
}

Calling this version of printType will print Derived for deri and Base for base. This is most useful in template mixins, which we'll see later in the chapter.

Template this parameters can also be used in struct declarations, though their usefulness is more limited given that structs in D are not polymorphic. However, a possible use case is demonstrated in the following declaration:

struct TypeMe {
  void printType(this T)() const {
    writeln(T.stringof);
  }
}

As printType is declared as const, it can be called on any instance of TypeMe, whether it was declared as const, immutable, or unqualified. The template this parameter can be used to determine which:

const(TypeMe) ct;
immutable(TypeMe) it;
TypeMe t;
ct.printType();
it.printType();
t.printType();
..................Content has been hidden....................

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