Templates in C++ primer (3)

In this article we’re going to look at how to output a std::tuple to a std::ostream, such as the familiar std::cout. The method used is an example of TMP (template metaprogramming), where the compiler generates code for us at run-time. In this case we need it to output every element of a std::tuple (with a separator) in a fully generic (any combination of types and tuple sizes) and type-safe way (as we would expect from C++).

But let’s not get ahead of ourselves. Firstly, let’s appreciate that templates can be used to output a std::pair (the element type for all associative containers, thus giving this code a use case). The actual effort involved is not great, literally just a one-line function (template):

template<typename T, typename U>
ostream& operator<<(ostream& os, const pair<T,U>& p) {
    return os << '(' << p.first << ',' << p.second << ')';
}

Note: two Standard headers are required for this code to compile, <ostream> (implied by <iostream>) and <utility> (implied by any associative container header), and also appropriate using-statements.

However, we’re not done with this, as std::strings are not printed out enclosed in quotes, for example. Programmers coming from other languages may be tempted to use RTTI (run-time type identification), but those with C++ knowledge would probably prefer function template specializations. The code below uses specializations of a new function to_ostream() to get the job done, while the function template to output std::pair becomes slightly longer:

template<typename T>
void to_ostream(ostream& os, const T& obj) {
    os << obj;
}

template<>
void to_ostream(ostream& os, const string& s) {
    os << '\"' << s << '\"';
}

template<>
void to_ostream(ostream& os, const char& c) {
    os << '\'' << c << '\'';
}

template<typename T, typename U>
ostream& operator<<(ostream& os, const pair<T,U>& p) {
    os << '(';
    to_ostream(os, p.first);
    os << ',';
    to_ostream(os, p.second);
    os << ')';
    return os;
}

Note: the <string> header is needed, in addition to those previously mentioned, for this code to compile.

Now it’s time to introduce (variadic) template parameter packs. Just as printf() takes any number of arguments (from one upwards), a template (type) parameter pack can hold zero or more types. The following program demonstrates how a single parameter can be “peeled off” and output by itself, knowing that the rest will follow:

// parameter_pack.cpp : demonstrate forwarding of variadic parameter packs

#include <iostream>

using namespace std;

template<typename T> // note: base case must be declared or defined before general case
void print(const T& head) {
    cout << head;
}

template<typename T, typename... Ts>
void print(const T& head, const Ts... tail) {
    cout << head << ' ';
    print(tail...); // note: recursive function call
}

int main() {
    print(1.0, "man", "went", 2, "mow", '\n');
}

The syntax uses ellipses (...) and takes a bit of getting used to. Notice that a parameter pack can be assigned to a variable, and forwarded to a function, but can’t be accessed directly.

Putting these together, and going back to our original goal of outputting a std::tuple, we might try to combine operator<< with a call to a recursive print function, something like this:

template<size_t N, typename...Args>
void print(ostream& os, const tuple<Args...>&t) {
    os << get<sizeof...(Args) - N>(t) << ' ';
    print<N - 1, Args...>(os, t);
}

template<typename... Args>
ostream& operator<<(ostream& os, const tuple<Args...>& t) {
        print<sizeof...(Args),Args...>(os, t);
        return os;
}

So, this code compiles as shown here, but does not work outputting a sample std::tuple, complaining of “tuple index out of bounds”, or similar. The reason is simple, we’ve inadvertently caused infinite recursion! Clearly we need a base case of print(), but the solution is not as simple as we may like because function templates cannot be partially specialized. (Note: see the end of this article for the corrected code, utilizing if constexpr.)

We can however use a “helper struct”, which provides a print() function as a (public) static member. The specialization of the struct is now permitted; print() is a function template of a class template (the template keyword is used at two levels):

template<size_t N>
struct TupleOstream {
    template<typename... Args>
    static void print(ostream& os, const tuple<Args...>&t) {
        os << get<sizeof...(Args) - N - 1>(t) << ", ";
        TupleOstream<N - 1>::print(os, t);
    }
};

template<>
struct TupleOstream<0> {
    template<typename... Args>
        static void print(ostream& os, const tuple<Args...>&t) {
        os << get<sizeof...(Args) - 1>(t);
    }
};

This code now compiles, links and runs correctly. To aid understanding, you could compare the logic and syntax with the (non-compiling) print() from before. Also, get your “off-by-one” Math correct; we could have counted down to one, however TMP recursion ending with <0> (instead of <1>) is probably better style.

Here is the final version of the tuple-printer (using a struct) as a complete program:

// print_tuple1.cpp : output a tuple using class and member function templates with TMP

#include <iostream>
#include <tuple>

using namespace std;

template<size_t N>
struct TupleOstream {
    template<typename... Args>
    static void print(ostream& os, const tuple<Args...>&t) {
        os << get<sizeof...(Args) - N - 1>(t) << ", ";
        TupleOstream<N - 1>::print(os, t);
    }
};

template<>
struct TupleOstream<0> {
    template<typename... Args>
        static void print(ostream& os, const tuple<Args...>&t) {
        os << get<sizeof...(Args) - 1>(t);
    }
};

template<typename... Args>
ostream& operator<<(ostream& os, const tuple<Args...>& t) {
    if constexpr (sizeof...(Args) == 0) {
        return os << "{}";
    }
    else {
        os << "{ ";
        TupleOstream<sizeof...(Args) - 1>::print(os, t);
        return os << " }";
    }
}

int main() {
    tuple my_tuple{ "Hi", 2, 0xALL, 0U, "folks!" };
    cout << my_tuple << '\n';
}

So, that was use of TMP with parameter packs demonstrated. (As an exercise you could incorporate the to_ostream() function from earlier.) Next time we’ll look at a use case for SFINAE.

Update: 2021/07/05: It occurred to me soon after this article was published that a way to break the infinite recursion in the global print() function (called from operator<<) could be to use if constexpr to make the recursive function call conditional. This type of conditional test “lives in both worlds”, as it can be evaluated at both compile-time and run-time. Below is the modified version which works exactly like our struct TupleOstream above (the main() function is identical, as is the output, and operator<< requires only one line to be modified):

// print_tuple2.cpp : output a tuple using recursive call and constexpr condition test

#include <iostream>
#include <tuple>

using namespace std;

template<size_t N, typename... Args>
void print(ostream& os, const tuple<Args...>&t) {
    os << get<sizeof...(Args) - N - 1>(t);
    if constexpr (N > 0) {
        os << ", ";
        print<N - 1, Args...>(os, t);
    }
}

template<typename... Args>
ostream& operator<<(ostream& os, const tuple<Args...>& t) {
    if constexpr (sizeof...(Args) == 0) {
        return os << "{}";
    }
    else {
        os << "{ ";
        print<sizeof...(Args) - 1,Args...>(os, t);
        return os << " }";
    }
}

int main() {
    tuple my_tuple{ "Hi", 2, 0xALL, 0U, "folks!" };
    cout << my_tuple << '\n';
}

So now we have two ways of accomplishing the same goal, the “traditional” C++11-esque way (std::tuple was introduced with this Standard) and the more Modern C++17-and-later way. This is an example of how constexpr can be used when writing Modern C++, as an alternative to more established (but arguably more arcane) TMP methods. (Note that N still counts down from sizeof...(Args) - 1 to 0, however this is no longer the only way of accomplishing the task it could be made to count the other way.)

In the final article in this mini-series we’ll take a look at SFINAE in more detail, and how it can be used to turn template instantiation on or off.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s