Templates Infinity Theory – From C++11 to C++20 – Part 2

C++ standard keep drawing attention to the metaprogramming abilities, syntax, and features. At the last article we saw how to handle variadic templates since C++11. In this article we’ll see how C++17 fold-expression feature helps us with this mission, and significantly simplifying the expressions [sometimes].

Previous article in series: Templates Infinity Theory – From C++11 to C++20 – Part 1
Next article in series: The Exact Solution for a Generic Problem – Part 1

C++17 – Fold-Expressions

Before we dig deeper into this subject, an important thing to keep in mind: It’s gonna seem really cool, and you might think that from now on, you’ll never use the old recursive way again, but keep in mind that this feature not always work the way you think it is.

C++17 brought us a great feature called “fold-expressions” that its only purpose is to help us with variadic templates. It helps us to apply the same expression we applied for the first element in the packed params, to all of the params, and to apply a connection between them using operators.

Syntax:
1. ( pack op ... )
2. ( ... op pack )
3. ( pack op ... op init )
4. ( init op ... op pack )

opany of the following 32 binary operators: + - * / % ^ & | = < > << >> += - && || , .* ->* and more… In a binary fold, both ops must be the same.
packan expression that contains an unexpanded parameter pack and does not contain an operator with precedence lower than cast at the top level (formally, a cast-expression)
initan expression that does not contain an unexpanded parameter pack and does not contain an operator with precedence lower than cast at the top level (formally, a cast-expression)
From cppreference – fold expression

Have a look over our print function from the previous article:

template <typename T>
void print(T &&val) {
    std::cout << val << std::endl;
}

template <typename T, typename ...Args>
void print(T &&val, Args&& ...args) {
    print(val);
    print(args...);
}

We actually needed to write with a recursion syntax (Note: at run-time it’s not really a recursion, because we are talking about different function specializations), and to create two functions to archive what we wanted. Also, at run-time we’ll have a great overhead when we jump between functions. It seems like a bit too much for consideration.

With fold expression, we are able to simplify it to a single function, with less overhead, and with faster compilation time:

template <typename ...Args>
void print(Args&& ...args) {
    ((std::cout << args << "\n"), ...)  << std::endl;
}

To understand the syntax here we’ll go from the inside out:

(std::cout << args << "\n")

args is the unexpanded parameter pack, which makes this expression the pack expression.

The comma after the right parenthesis, is what we called earlier op.

This means, that the final step of rewriting our code at the compilation time result, would be the same pack expression with arg_0, arg_1, …, arg_N. For example- for the call:

print("aa", 3, 6.2);

Our “final” code will be (the instantiated version):

template <>
void print<const char(&)[3], int, double>(const char (&arg_0)[3], int &&arg_1, double &&arg_2) {
    (std::cout << arg_0 << "\n", (std::cout << arg_1 << "\n", std::cout << arg_2 << "\n")) << std::endl;
}

Note: It’s not really the final code- the operators will also be transform into std::operator<<(std::cout, arg_0). However due to this code readability, and the important point in this example, I showed am equally version without this transformation.

Something you might been noticed is the parentheses amount, why there are so many of them? In order to understand that, we’ll have to look at the fold expression instantiation rules:

  1. Unary right fold (E op ...) becomes (E1 op (... op (EN-1 op EN))).
  2. Unary left fold (... op E) becomes (((E1 op E2) op ...) op EN).
  3. Binary right fold (E op ... op I) becomes (E1 op (... op (EN−1 op (EN op I)))).
  4. Binary left fold (I op ... op E) becomes ((((I op E1) op E2) op ...) op EN).

A common mistake is the actual difference between the rules 1 & 2. It looks simple, intuitively I thought that the difference between them is like a reverse recursion, and that in order to reverse the printing, all I have to do is so:

template <typename ...Args>
void print(Args&& ...args) {
    (..., (std::cout << args << "\n"))  << std::endl;
}

Well, this is actually a mistake. The only difference here is the parentheses. So, if it’s not the easy way to reverse the arguments printing, what is? There is no easy way to tell it, so I’ll make it fast: This is not the right case for fold expression. OOPS! It’s almost there, but it’s not (and don’t tell me I didn’t warn you). Back to our originally implementation:

template <typename T>
void print(T &&val) {
    std::cout << val << std::endl;
}

template <typename T, typename ...Args>
void print(T &&val, Args&& ...args) {
    print(val);
    print(args...);
}

To make it printing arguments in a reverse order, all we have to do is to switch between lines 8 & 9.

template <typename T>
void print(T &&val) {
    std::cout << val << std::endl;
}

template <typename T, typename ...Args>
void print(T &&val, Args&& ...args) {
    print(args...);
    print(val);
}

And just like in a recursion magic, it worked.

Another interesting example is to implement an arguments divider:

template <typename ...Args>
auto divide(Args&& ...args) {
    return (args / ...);
}

Interesting, but when I first looked at it, I weren’t pretty sure if it does what I want it to- because unlike our previous example, here parentheses do matter.

For the input: divide(1., 2., 3.), this example will produce 1.5 [1. / (2. / 3.)], and for the same input, the return statement: return (... / args); will produce 0.166667 [(1. / 2.) / 3.]. “The devil is in the details” – don’t let him catch you unprepared.

Variadic Template Class

In the previous article, I showed an example of variadic template inheritance, and we saw unfriendly expression, that help us to go through our inheritances, and to call a common function in every one of them:

template <typename ...Properties> // Variadic Template
class shape : virtual public Properties... { // Multiple properties inheritances, the virtual will be more significant in the future
public:
    virtual ~shape() = default;
    virtual void input_data() {
        // Thanks to: https://stackoverflow.com/a/36101305/8038186 for C++11 solution
        using expand = int[];
        static_cast<void>(expand{ 0, (Properties::input_data(), void(), 0)... });
    };
    [[nodiscard]] virtual double area() const = 0;
};

Once again, fold expressions here to help:

virtual void input_data() {
    (Properties::input_data(), ...);
};

Now it seems a lot more nicer than before.

But there is a thing we didn’t discuss about.. What if someone by mistake, wouldn’t pay attention, and will call this class with a class that doesn’t have this input_data function? Of course, we could create a base property class that every property should inherit from:

class shape_property {
public:
    virtual void input_data() = 0;
};

class height_property : public shape_property {
public:
    void input_data() override { std::cout << "Enter height:" << std::endl; std::cin >> height; };

protected:
    double height;
};

class width_property : public shape_property {
public:
    void input_data() override { std::cout << "Enter width:" << std::endl; std::cin >> width; };

protected:
    double width;
};

template <int base_number> // Just a little creativity to make "infinite" bases
class base_property : public shape_property {
public:
    void input_data() override { std::cout << "Enter base " << base_number << ":" << std::endl; std::cin >> base; };

protected:
    double base;
};

But the following case is still possible:

class a_mistake_property {
public:
    void read_data() { /* ... */ }
protected:
    double my_important_data;
};

// ...

class my_shape : public shape<a_mistake_property> {
public:
    void input_data() override {
        shape::input_data();
    }
};

And.. What a surprise:

error: ‘input_data’ is not a member of ‘a_mistake_property’
   line |         (Properties::input_data(), ...);

Well, go ahead a let the buggy coder fix the mistake.. In the best case, the bugger will know exactly what the problem is, and will fix it. In the worst case? Prepare yourself for a sleepless night when you are at home, trying to explain the bugger what is wrong with “the compiler”, in a code you wrote two years ago. Wouldn’t it be nicer to get a more descriptive error? Something like “The template param does not fit the conditions: base class of shape_property”?

Template params restrictions

This is a preview for the next post in this series.
Sometimes we want to limit our template usages and available specializations, and we don’t want as well to specify ahead each and every legal specialization. To archive it, we’ll need a way to apply some restrictions to our template params.

Assume that you want to enable an inheritance from variadic template, but you want as well to inherit only from a specific interface, something like that:

class interface {};

class derived_1 : public interface {};
class derived_2 : public interface {};
class independent_class {};

template <interface ...Implementations> // Compilation error
class my_class : Implementations... {};

The above example won’t compile. interface will make this template a non-type template, and class can’t be in this position. In the next article in this series we’ll see how can we do it today with C++20 [hint: concepts], and how it could be done before C++20 came to our help.

More posts in this series:

4 thoughts on “Templates Infinity Theory – From C++11 to C++20 – Part 2

Leave a comment