Professional CMake:

A Practical Guide

Learn to use CMake effectively with practical advice from a CMake co-maintainer. You can also have the author work directly with your team!

Practical uses for variadic templates

You might not have even heard of parameter packs or variadic templates before, but if you’ve worked with C++ templates for a while, chances are at some point you’ve wanted the functionality they provide. This post provides a few practical examples showing some of the problems they solve and techniques they enable.

Variadic template basics

Variadic templates were added to the language in C++11. Simply put, they allow class and function templates to have a varying number and type of parameters. The way this is done is similar to variable argument lists for ordinary non-template functions (think printf, etc.), using an ellipsis (…) in the parameter list to indicate the variable template parameters. Here’s a simple example of a class and function taking a variable number of template parameters:

template<typename ... TArgs>
class MyClass { /* */ };

template<typename ... TArgs>
void myFunc(TArgs ... args) { /* */ }

In the above, the ellipses tell the compiler that, rather than being a single type or value, TArgs is a (possibly empty) list of types or values. C++ calls this list a parameter pack. Client code would use MyClass or myFunc just like any ordinary template. For example:

MyClass<int,float> myIntFloat;
MyClass<std::string> myString;
MyClass<> myNothing;

myFunc(23, 16.7);
myFunc("Hello World");
myFunc();
myFunc<double,float>(23.7, 42.9);

In the case of functions, the compiler can perform template argument deduction to work out the type of each argument in the parameter pack, just like it would for ordinary template functions. Of course, you can also explicitly specify the types as in the last line of the example above.

Inside a class or function, the parameter pack can be used in a variety of ways. We will only look at some of these here, but the interested reader may wish to explore the full list as detailed elsewhere. For our purposes, we will use some specific examples to demonstrate some more common and useful techniques.

Example 1: Forwarding arguments

Consider the case where you want the creation of various classes of objects to be performed through a template factory function which we will name create. The classes to be created may have nothing in common with each other. You may wish to do this, for example, to control memory allocation or perform some sort of object tracking on classes which do not inherently have such capabilities. Without variadic templates, you are immediately faced with the problem of how to handle the arguments to pass to the constructor of each type being created. The constructors could have different numbers of parameters with different types. The typical way this is handled is to define a family of template functions, each with a different number of arguments but with essentially the same implementation. This results in duplicating the same implementation logic each time and also has the limitation of only supporting constructors with up to the provided number of parameters.

Ideally, we’d like to implement create just once and let the compiler forward all our arguments to the relevant constructor. Variadic templates allow us to do exactly that. Here is one such example where we log a message each time an object is created, printing the name of the type:

template<typename T, typename ... TArgs>
T* create(TArgs ... args)
{
    std::cout << "Creating " << typeid(T).name() << std::endl;
    T* obj = new T(args ...);
    return obj;
}

class A
{
public:
    A(int x, double y = 42.3);    // (1)
    A();                          // (2)
};

class B
{
public:
    B(const std::string& s);      // (3)
};

A* a1 = create<A>();              // Calls (2)
A* a2 = create<A>(16, 9.4);       // Calls (1)
A* a3 = create<A>(23);            // Calls (1)
A* a4 = create<A>("Hi mum!");     // Error

B* b1 = create<B>("Hi mum!");     // Calls (3)
B* b2 = create<B>(std::string()); // Calls (3)
B* b3 = create<B>();              // Error

In the implementation of the create function, note the arguments passed to the T constructor. The trailing … after the args name tells the compiler to expand the args parameter pack to the list of arguments it represents. This expanded set of arguments are then passed to the constructor, so the compiler has to match the number and type of expanded arguments to a suitable constructor for the type T.

In the example above, a1-3 all result in a call to one of the two defined constructors. The a4 call expands the parameter pack to a single const char* argument, but A has no constructor which accepts such an argument. Similarly, b1-2 matches the provided constructor in B, with b1 automatically converting its const char* argument to std::string. The b3 case fails because B has no default constructor.

The use of variadic templates here has allowed a single implementation of create to handle all possible types and all constructors for each type. This technique isn’t just limited to constructors, it can be used with any function, but construction is probably one of the more common uses.

The above technique is even used by C++ itself. C++11 added the emplace_back and emplace functions to the STL containers. These work just like push_back and insert respectively, except that they construct the new objects directly in place, thereby avoiding any copy or move operation.

Example 2: Inheritance

It is also possible to use parameter packs to represent the set of base classes to derive a subclass from. For example:

template<typename ... Mixins>
class X : public Mixins...
{
public:
    X(const Mixins&... mixins) : Mixins(mixins)... {}
};

// Assume A, B and C are defined elsewhere
// and they have default constructors
A a;
B b;
C c;
X<A,B> xAB(a, b);
X<B,A> xBA;   // Error, needs arguments
X<C> xC(c);
X<> xNoBases;

There’s a bit going on in this example, so let’s break it down. The parameter pack Mixins is expanded so that each type it contains will become a base class of X. Note, however, that the order in which they are specified matters, since changing the order results in a distinctly different type. For this reason, the technique as presented here is a fairly fragile way to specify base classes, but it can be useful when employed as part of template implementations which are not exposed to calling code (e.g. to enforce constraints internally).

The constructor for X is a little more complex. It contains two parameter pack expansions:

const Mixins& ... mixins
Mixins(mixins)...

A parameter pack expansion is just a pattern and an ellipsis. Simplistically, the pattern is just what is left when the ellipsis is removed. The pattern is instantiated once for each item in the parameter pack. For both of the above expansions, Mixins is replaced with each type from the parameter pack and mixins acts as a placeholder for the parameter name. For example:

// The constructor for X<A,B,C> effectively expands to:
X(const A& a, const B& b, const C& c) : A(a), B(b), C(c) {}

Example 3: Iteration of dissimilar types

A fundamental part of the STL is the ability to iterate over a range of objects of the same type. Initializer lists take this further by providing container-like capabilities for a list of objects defined at compile time (see my previous posts here and here for more on these). Sometimes, however, you may want to iterate over a set of objects which may have nothing in common with each other, even being completely unrelated types. As an example, consider the following code which prints an arbitrary list of objects as a set of fixed-width strings. It uses std::ostringstream to convert each object to a string of a specified width.

template<int width>
std::string padFormat()
{
    return "";
}

template<int width, typename T>
std::string padFormat(const T& t)
{
    std::ostringstream oss;
    oss << std::setw(width) << t;
    return oss.str();
}

template<int width, typename T, typename ... Types>
std::string padFormat(const T& first, Types ... rest)
{
    return padFormat<width>(first) + padFormat<width>(rest...);
}

std::cout << padFormat<8>("one", 2, 3.0) << std::endl;
std::cout << padFormat<4>('a', "BBB", 9u, -8) << std::endl;

The key to the padFormat function is its use of recursion to iterate through the set of parameters. The general template gives the first function argument a specific name and the remaining arguments are encapsulated in a parameter pack. When padFormat calls itself with just that parameter pack, the recursive call has the effect of picking off the first element and recursing again with the rest of the parameter pack. The padFormat specialization taking a single parameter defines how each object in the parameter pack should be handled individually, while the specialisation taking no parameters stops the recursion when all objects in the parameter pack have been processed.

A useful feature of this technique is that the parameter expansion happens at compile time, so there is ample opportunity for the compiler to optimize each call to padFormat. This arrangement also has the added advantage that further specializations can be provided for specific types to override how they are processed instead of using the default method employing std::ostringstream.

Example 4: Initializer lists

Interestingly, there is another way to effectively iterate through a set of objects in a parameter pack. The behaviour of the previous example can also be implemented in the following more concise manner:

template<int width, typename... TArgs>
void printPadFormatted(TArgs ... args)
{
    auto dummy = { (std::cout << std::setw(width) << args, 0)... };
    std::cout << std::endl;
}

printPadFormatted<8>("one", 2, 3.0);
printPadFormated<4>('a', "BBB", 9u, -8);

The body of printPadFormatted contains just one parameter pack expansion which has the pattern:

(std::cout << std::setw(width) << args, 0)

The key to why this works lies in the outer parentheses in combination with the comma operator before the 0. The parentheses ensure that the whole expression is treated as the pattern. The comma operator causes the streaming to std::cout to be performed before assigning 0 as the result of each expanded pack object. In effect, the work to be performed on each object is done as a side effect of creating an entry in dummy with value 0 for each item. You could put pretty much any valid expression before the comma to do the actual work on each object.

This alternative technique has the advantage of being much more concise, but it does not offer a specific customization point for handling specific types differently other than a generic std::ostream formatting override. It may also be somewhat less familiar to newer C++ developers who may not be accustomed to thinking of a comma as an operator rather than simply a list separator.

Example 5: Tuples

C++11 also added various things which take advantage of variadic templates as part of the language itself. A particularly notable example is the std::tuple class template, which is a generalization of std::pair. These are a very convenient and compact way to do things like return multiple values from a function. Whereas an ordinary std::pair only allows two values to be returned, std::tuple supports returning any number of arguments. To return more than two values using just std::pair, you have to resort to recursive techniques, but accessing values then becomes cumbersome. The new std::tuple class makes this easy.

template<typename T>
std::tuple<double,double,double> computeStats(const T& container)
{
    if (container.empty())
        return std::make_tuple(0.0, 0.0, 0.0);

    double minVal = *std::min_element(begin(container), end(container));
    double maxVal = *std::max_element(begin(container), end(container));
    double aveVal = std::accumulate(begin(container), end(container), 0.0)
                     /container.size();

    return std::make_tuple(minVal, maxVal, aveVal);
}

std::vector<double> values = {3.4, 7.2, -9.8, 6.4};
auto stats = computeStats(values);
std::cout << "Min: " << std::get<0>(stats) << "n"
          << "Max: " << std::get<1>(stats) << "n"
          << "Ave: " << std::get<2>(stats) << std::endl;

The std::get function is how a particular element of a tuple can be obtained, with the index of the desired element as the template argument. This is in contrast to std::pair which provides the less flexible first and second data members.

Related to std::tuple is std::tie, which allows individual elements of a returned tuple to be assigned directly to specific variables without having to use separate calls to std::get:

double minVal;
double maxVal;
double aveVal;
std::vector<double> values = {3.4, 7.2, -9.8, 6.4};
std::tie(minVal, maxVal, aveVal) = computeStats(values);
std::cout << "Min: " << minVal << "n"
          << "Max: " << maxVal << "n"
          << "Ave: " << aveVal << std::endl;

And lastly, you can use std::ignore as a placeholder with std::tie for elements you are not interested in:

double aveVal;
std::vector<double> values = {3.4, 7.2, -9.8, 6.4};
std::tie(std::ignore, std::ignore, aveVal) = computeStats(values);
std::cout << "Ave: " << aveVal << std::endl;

Closing comments

The examples in this post highlight some of the techniques that variadic templates enable. They are a useful way of handling situations where not just the types but also the number of template parameters can be varied. There are other contexts where parameter packs can be used in addition to those shown here, such as in lambda captures, exception specifications, alignment specifiers and attribute lists. Furthermore, it is possible to have nested parameter expansions, a technique not explored in this post. The techniques shown here should at least provide a practical introduction to using variadic templates in real world situations.

3 thoughts on “Practical uses for variadic templates”

  1. Great educative article. I am learning c++ from internet sources

    Why doesn’t first example compile.error undefined reference to A::A() etc
    Does template need to go in header file.?

    Reply
    • Example 1 shows just the minimal amount of detail needed to demonstrate the concepts. The actual implementations of the constructors for A and B were omitted, but you could add them yourself and it will link fine (I just tried it to confirm).

      Reply
  2. Excellent article…thank you. This wouldnt be my favourite c++ addition purely because I find the non trivial use cases tricky sometimes. Your article alleviates some of that trickiness.

    Reply

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.