Templates in action

Contents

  1. Variadic templates
  2. Type traits
  3. Overload control
  4. SFINAE
  5. Type Erasure
  6. Static Polymorphism
  7. Expression templates
  8. Compile-time computation

Variadic templates

Allow much easier implemenation of templates with variable arguments, like std::tuple, std::function, etc.

Parameter pack

// loading ...

Parameter pack

  • can match remaining arguments
  • can be expanded
  • can not be stored in a variable or manipulated

How to use a parameter pack

// loading ...

How to use a parameter pack

// T is a template parameter pack
sizeof...(T)
// count of types in T

Sample

A console that allows execution of arbitrary std::functions.

// loading ...

// loading ...

So what does the magic wrap do?

// loading ...

// loading ...

// loading ...

Using variadic templates

Based on "The Way of the Exploding Tuple"

// loading ...

// loading ...

// loading ...

Type Computations

  • given existing type T, create a new one based on T
  • given T determine its properties (traits):
    • a pointer?
    • polymorphic?
    • Plain old data (POD)?

Type traits

The general name for the type computational facilities. The standard type traits are defined in type_traits header.

Most of the type traits use template specialization.

Helpers:

template <typename T, T Value>
struct integral_constant {
    static T value = Value;
};

typedef integral_contant<bool, false> false_type;
typedef integral_contant<bool, true> true_type;

Is pointer?

template <typename T>
struct is_pointer : false_type
{};

template <typename T>
struct is_pointer<T*> : true_type
{};

Add pointer

template <typename T>
struct add_pointer
{
    typedef T* type;
};

The resulting new type is defined in ::type.

Remove pointer

template <typename T>
struct remove_pointer
{
    typedef T type;
};

template <typename T>
struct remove_pointer<T*>
{
    typedef T type;
};

type_traits

  • is_arithmetic
  • is_fundamental
  • is_polymorphic

Practice

  • use std::false_type and std::true_type for custom type traits
  • always define the result of type transformation in type

Controlling the overload set

The overload set is the set function overloads that are viable for resolving a call to a function

Type traits and tags

template <typename T>
void Copy(T* source, T* destination, size_t count);
template <typename T>
void Copy(T* source, T* destination, size_t count) {
    for (size_t i = 0; i != count; ++i) {
        destination[i] = source[i];
    }
}

Can we do better?

template <typename T>
void Copy(T* source, T* destination, size_t count) {
    if ((T is POD)) {
        std::memcpy(destination, source, count * sizeof(T));
    }
    else {
        for (size_t i = 0; i != count; ++i) {
            destination[i] = source[i];
        }
    }
}

Overloads

Overloads are distinguished and chosen based on their arguments count and type. So we can add another argument and overload on it.

struct isPOD {};
struct nonPOD {};

template <typename T>
void Copy(T* source, T* destination, size_t count, isPOD) {
    std::memcpy(destination, source, count * sizeof(T));
}

template <typename T>
void Copy(T* source, T* destination, size_t count, nonPOD) {
    for (size_t i = 0; i != count; ++i) {
        destination[i] = source[i];
    }
}
int a[10], b[10];
Copy(a, b, 10, isPOD());
// choses the memcpy implementation, because that is matching
// overload

std::string c[10], d[10];
Copy(c, d, 10, isPOD()); // oops

Not very nice

  • requires extra parameter for the client to use
  • error prone
  • requires definition of extra structs
namespace detail
{
    template <typename T>
    void Copy(T* source, T* destination, size_t count,
    std::true_type /*isPOD*/) {
        std::memcpy(destination, source, count * sizeof(T));
    }
    
    template <typename T>
    void Copy(T* source, T* destination, size_t count,
    std::false_type /*isPOD*/) {
        for (size_t i = 0; i != count; ++i) {
            destination[i] = source[i];
        }
    }
}
template <typename T>
void Copy(T* source, T* destination, size_t count)
    detail::Copy(source, destination, count, std::is_pod<T>());
}

SFINAE

Substitution Failure Is Not An Error

The compiler doesn't generate compiler error when a template paratemeter can not be substituted with a concrete type, it simply ignores the substutition.

template <typename T>
typename std::enable_if<std::is_pod<T>::value, void>::type
Copy(T* source, T* destination, size_t count) {
    std::memcpy(destination, source, count * sizeof(T));
}

template <typename T>
typename std::enable_if<!std::is_pod<T>::value, void>::type
Copy(T* source, T* destination, size_t count) {
    for (size_t i = 0; i != count; ++i) {
        destination[i] = source[i];
    }
}
template <bool Value, typename T>
struct enable_if {
};

template <typename T>
struct enable_if<true> {
    typedef T type;
};
int s[10], d[10];
Copy(s, d, 10);

int is POD

  • enable_if<is_pod<int>::value, void>::type is void and the memcpy overload of Copy is viable
  • There is no such type in enable_if<!is_pod<int>::value, void>, so the substitution fails and the compiler silently ignores the for overload
template <typename T>
typename std::enable_if<!std::is_pod<T>::value, void>::type
Copy(T* source, T* destination, size_t count) {
    for (size_t i = 0; i != count; ++i) {
        destination[i] = source[i];
    }
}

SFINAE

// loading ...

SFINAE

Use std::enable_if to control the overload set of generic functions.

Type erasure

  • C++'98 doesn't have C# style delegates.
  • In C++'11 they are provided by std::function and std::bind

How they can be implemented?

// loading ...

// loading ...

// loading ...

// loading ...

// loading ...

DelegateFun<void()> and DelegateMethod<A, void()> can not be a single class, because they have different template arguments

Yet they have the same behavior - the void () function.

To make a usable delegate class, we need to erase the type difference between the types.

// loading ...

// loading ...

// loading ...

// loading ...

// loading ...

// loading ...

The pattern for type erasure is to define a class that exposes the API and forwards all calls to a polymorphic object, which implements the concrete API.

std::function, boost::any are probably the most famous examples of type erasure.

The above implementation is pretty low performant.

The reason for this is that a lot of small objects get allocated and always the actual function is away from the Delegate instance we have.

This can be resolved by storing the concrete DelegateFun or DelegateMethod in a small buffer, directly in the Delegate class.

Static Polymorphism

CRTP

Curiously Recurring Template Pattern

class Derived : public Base<Derived>
{
};

We have already seen that:

struct Renderer : std::enabled_shared_from_this<Renderer>
{};

struct Texture : RefCounted<Texture>
{};

The CRTP allows the base template to use the derived type - it can call methods, create/destroy instances and so on.

Static polymorphism

Using the derived class implementation though the base class interface

  • without virtual calls
  • compile-time

Runtime polymorphism

struct JSONParser {
    virtual void array_begin() = 0;
    virtual void array_end() = 0;
    bool Parse() {
        //...
        char n = next();
        switch (n) {
            case '[' : array_begin(); break;
            case ']' : array_end(); break;
            // ...
        }
    }
};

Runtime polymorphism

struct JSONMinifier : JSONParser {
    virtual void array_begin() override {
        output.put('[');
    }
    virtual void array_end() override {
        output.put(']');
    }
};

Runtime polymorphism

We are making a virtual call for every token in the JSON file. And it is unlikely that we need more than a few different JSON parser types.

Static polymorphism

template <typename Derived>
struct JSONParser {
    Derived* This() {
        return static_cast<Derived*>(this);
    }
    bool Parse() {
        //...
        char n = next();
        switch (n) {
            case '[' : This()->array_begin(); break;
            case ']' : This()->array_end(); break;
            // ...
        }
    }
};

Static polymorphism

struct JSONMinifier : JSONParser<JSONMinifier> {
    void array_begin() {
        output.put('[');
    }
    void array_end() {
        output.put(']');
    }
};

Barton-Nackman trick

Characterized by an in-class friend function declared in the base class of a CRTP

template <typename T>
class comparable {
    friend bool operator > (const T& lhs, const T& rhs) {
        return !(lhs < rhs);
    }
    friend bool operator == (const T& lhs, const T& rhs) {
        return !(lhs < rhs || lhs > rhs);
    }
};

struct Poly : public comparable<Poly> {
    bool operator<(const Poly&rhs) const {
        //...
    }
}

Expression Templates

A technique for using types (templates) to represent part of an expression.

The type (template) represents some kind of an operation with its operands and allows the result to be evaluated later or passed to a function (used itself as an operand).

A Polynom class

// loading ...

A simple implementation

template <typename T>
Polynom<T> operator+(const Polynom<T>& lhs, const Polynom<T>& rhs) {
    auto power = std::max(lhs.power(), rhs.power();
    Polynom<T> result(power);
    for (auto i = 0; i != power; ++i) {
        result.set(i, lhs.get(i) + rhs.get(i));
    }
    return result;
}

How many loops and temporaries?

Polynom<int> a = { 1, 0, 1, 0, 1 }; // x^3 + x + 1
Polynom<int> b = { 2, 2, 0, 2, 3 }; // 3*x^3 + 2*x^2 + 2*x + 2
Polynom<int> c = { 42 };

auto d = a + b + c;
// compiled to:
// t1 = a + b
// t2 = t1 + c
// d = t2
  • 2 temporaries
  • 2 loops!
  • heap allocations!

Expressions

auto d = a + b + c;
// With ET it compiled to:
// Sum(a,b) t1 = a + b
// Sum(Sum(a,b),c) t2 = t1 + c
// d = t2 // create Polynom from Sum(Sum(a, b), c)
// Pseudo code ahead
auto power = std::max(a.power(), b.power(), c.power())
Polynom<int> d(power);
for (auto i = 0; i != power; ++i) {
    d.set(i, a.get(i) + b.get(i) + c.get(i));
}
  • 2 temporaries
  • 1 loop!
  • 1 heap allocation!

Sum

// loading ...

Multiplication

// loading ...

Simple expressions

// loading ...

All possible expressions

// loading ...

Bonus question

Why does the Polynom template have methods get and set instead of operator[]?

Compile-time computation

http://cpptruths.blogspot.com/2011/07/want-speed-use-constexpr-meta.html