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

One of the great abilities of metaprogramming in C++ is variadic templates. The ability to code a function or a class that accept unknown parameters amount of unknown types, redefine the “Generic” word. How can we code a function without any knowledge of its params? How can we use it to gain new abilities? How did we handle it back in the days [C++11], and how do we do it today [C++20]?

Note: The original plan was to talk also about fold-expressions in this article, but due to the length and the amount of content even without this part, I decided to separate this article to 2 parts. In this part we’ll discuss about variadic templates- Usages & advantages.

Previous article in series: C++ – Partial / Explicit specialization
Next article in series: Templates Infinity Theory – From C++11 to C++20 – Part 2

Currently in series:

Variadic Template

C++11 brought to us the ability to pack/unpack params using variadic templates. This ability is what making a call to the same function, with different types/params count without re-implementing it, possible & readable. Some of you probably wondering right now about the C function printf that working in the exact same way, however the way C implemented this ability is not in the scope of this article, so comment here if you want me to publish an article about it.

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...);
}

int main() {
    print("aa", 3, 1.6);
}

The above example prints in separated lines aa 3 1.6, we’ll follow it step by step:

print("aa", 3, 1.6); – Compiler deduce the print function ID: print<const char[3], int, double>("aa", 3, 1.6);, and begin with the instantiation process:

template <> void print<const char[3], int, double>(const char (&val)[3], int &&arg_0, double &&arg_1) {
    print(val);
    print(arg_0, arg_1);
}

print(val) – Compiler deduce again the print function ID: print<const char[3]>(val);, and begin with the instantiation process:

template <> void print<const char(&)[3]>(const char (&val)[3]) {
    std::cout << val << std::endl;
}

print(arg_0, arg_1); – Compiler deduction: print<int, double>(arg_0, arg_1);, Instantiation process:

template <> void print<int&, double&>(int &val, double &arg_0) {
    print(val);
    print(arg_0);
}

And the compiler code result:

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

template <>
void print<const char(&)[3]>(const char (&val)[3]) {
    std::cout << val << std::endl;
}

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

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

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

template <> void print<const char[3], int, double>(const char (&val)[3], int &&arg_0, double &&arg_1) {
    print<const char(&)[3]>(val); // 2nd instantiation call
    print<int&, double&>(arg_0, arg_1);  // 3rd instantiation call
}

template <> void print<int&, double&>(int &val, double &arg_0) {
    print<int&>(val); // 4th instantiation call
    print<double&>(arg_0); // 5th instantiation call
}

int main() {
    print<const char[3], int, double>("aa", 3, 1.6); // 1st instantiation call
}

Now that we compiled the code we can go ahead and run it. At run-time we’ll face the same recursion we faced at compile time, only here it between function specializations. Fortunately, C++ didn’t stopped its meta-programming features development on C++11, and in C++17 we got a tool that will make things easier, faster and with less overhead for us sometimes.

Variadic Template Templates

Every template can be a variadic template, even if it inside another template:

template <template<typename...> typename Cont> void func();

This is a declaration of a function that accepts a type which accepts unknown template params. We saw in C++ basic templates usage – Part 2 an example:

template <typename OutContType, template <typename, typename> class C, typename InContType>
auto cast_all(const C<InContType, std::allocator<InContType>> &c) {
    C<OutContType, std::allocator<OutContType>> result(c.begin(), c.end());
    return result;
}
 
int main() {
    std::vector<double> double_vec = {1.5, 2.3, 3.6};
    std::vector<int> int_vec;
    int_vec = cast_all<int>(double_vec);
    // cast_all<int, std::vector, double>(double_vec);
    return EXIT_SUCCESS;
}

Assume we created a container which accepts only a single type parameter:

template <typename T>
class my_container {
    T values[50];
    // ... Iterators implementation ...
};

If we would try to pass it to out function we’ll get a compilation error, because our function requires a container that accepts 2 template params, and our container accepts only one. Another example that won’t work here is std::set, that accepts 3 template params: container type, comparator, and allocator.

To solve this issue, we have to generalize our container type in this function:

template <typename OutContType, template <typename...> typename C, typename InContType>
auto cast_all(const C<InContType> &c) {
    C<OutContType> result(c.begin(), c.end());
    return result;
}

int main() {
    std::vector<double> double_vec = {1.5, 2.3, 3.6};
    std::vector<int> int_vec;
    int_vec = cast_all<int>(double_vec);

    std::set<float> double_set = {5.1f, 3.2f, 6.3f};
    std::set<int> int_set;
    int_set = cast_all<int>(double_set);
    return EXIT_SUCCESS;
}

So we just earned here both more generalized function, and more readable function.

Inheritance from a Variadic Template

Inheritance from a template can make our class very flexible & dynamic. For example:

// Base Shape
template <typename Properties>
class shape : public Properties {
public:
    virtual ~shape() = default;

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

    [[nodiscard]] virtual double area() const = 0;
};

// # Property Example 1
class width_height_properties {
public:
    /* ... Get / Set methods ... */

    void input_data() {
        std::cout << "Enter width & height:" << std::endl;
        std::cin >> width >> height;
    };

protected:
    double width, height;
};

// # Property Example 2
class two_bases_height_properties {
public:
    /* ... Get / Set methods ... */

    void input_data() {
        std::cout << "Enter base_a & base_b & height:" << std::endl;
        std::cin >> base_a >> base_b >> height;
    };

protected:
    double base_a, base_b, height;
};

// Shape Example Using Properties Example 1
class rectangle : public shape<width_height_properties> {
public:
    void input_data() override {
        std::cout << "Input Rectangle:" << std::endl;
        shape::input_data();
    };

    [[nodiscard]] double area() const override {
        return width * height;
    };
};

// Shape Example Using Properties Example 1
class triangle : public shape<width_height_properties> {
public:
    void input_data() override {
        std::cout << "Input Triangle:" << std::endl;
        shape::input_data();
    };

    [[nodiscard]] double area() const override {
        return (width * height) / 2;
    };
};

// Shape Example Using Properties Example 2
class trapezoid : public shape<two_bases_height_properties> {
public:
    void input_data() override {
        std::cout << "Input Trapezoid:" << std::endl;
        shape::input_data();
    };

    [[nodiscard]] double area() const override {
        return (base_a + base_b) * height / 2;
    };
};

int main() {
    trapezoid tra;
    triangle tri;
    rectangle rec;

    tra.input_data();
    std::cout << tra.area() << std::endl;

    rec.input_data();
    std::cout << rec.area() << std::endl;

    tri.input_data();
    std::cout << tri.area() << std::endl;

    return EXIT_SUCCESS;
}

Now, let’s make this even more flexible, when we separate the properties to several independent classes. So we can use the same height implementation in all three shapes (and also in some other shapes):

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;
};

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

protected:
    double height;
};

class width_property {
public:
    void input_data() { 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:
    void input_data() {
        std::cout << "Enter base " << base_number << ":" << std::endl;
        std::cin >> base;
    };

protected:
    double base;
};

class rectangle : public shape<width_property, height_property> {
public:
    void input_data() override { std::cout << "Input Rectangle:" << std::endl; shape::input_data(); };

    [[nodiscard]] double area() const override { return width * height; };
};

class triangle : public shape<width_property, height_property> {
public:
    void input_data() override { std::cout << "Input Triangle:" << std::endl; shape::input_data(); };

    [[nodiscard]] double area() const override { return (width * height) / 2; };
};

class trapezoid : public shape<base_property<0>, base_property<1>, height_property> {
public:
    void input_data() override { std::cout << "Input Trapezoid:" << std::endl; shape::input_data(); };

    [[nodiscard]] double area() const override {
        return (base_property<0>::base + base_property<1>::base) * height / 2; // Pay attention to specify the specialized bases
    };
};

int main() {
    trapezoid tra; triangle tri; rectangle rec;
    tra.input_data(); std::cout << tra.area() << std::endl;
    rec.input_data(); std::cout << rec.area() << std::endl;
    tri.input_data(); std::cout << tri.area() << std::endl;

    return EXIT_SUCCESS;
}

The flexibility is amazing, and it will be really easy to maintain this code for future shapes without many bugs / mistakes. However, lines 7-8 are pretty annoying, isn’t there an easier way to call a common function for every inheritance from the variadic template params? Since C++17, YES there is! – and we’ll discuss about fold-expressions in the next post.

Conclusion

Variadic templates gives us a large flexibility, and helps us to maintain our code. It saves us a lot of code duplications, however “with great power comes great responsibility”, pay attention to pass only types you know how to handle or else you might find some really nice latent bugs. In a future post we will see how to apply template params restrictions in several old school / new techniques.

More posts in this series:

3 thoughts on “Templates Infinity Theory – From C++11 to C++20 – Part 1

Leave a comment