The Exact Solution for a Generic Problem – Part 1

One of the basic rules of programming, is to code a single solution that will solve many problems. This rule, when used the right way, can save unimaginable amount of time, lines of code and bugs. However, it can also be the responsible for the destruction of the complete code architecture. In this article, I’ll cover up some ways to avoid such destruction when using metaprogramming.

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

Once Upon a Time

There was a kingdom- The Kingdom of Code! This kingdom was build with tall and strong walls, and only the loyalties int, and float were able to pass those giant walls.

namespace code_kingdom {
    class integer_land {
    public:
        integer_land(int);
        int sum() { return std::accumulate(vec.begin(), vec.end(), 0); }

    private:
        std::vector<int> vec;
    };

    class float_land {
    public:
        float_land(float);
        int sum() { return std::accumulate(vec.begin(), vec.end(), 0.f); }
        
    private:
        std::vector<float> vec;
    };
}

One day (here comes the interesting part!) you showed up- A brilliant software architect who just found the light, and discovered a way to reduce all of the kingdom costs by a half (!!), and not only that, with the same solution this kingdom be able to treat more civilians and to accept taxes from double, size_t, and others.

The king was amazed, and ordered you to start the job immediately. After few days, you refactor the whole kingdom, and successfully created an amazing architecture. Waterfall of code lines, lakes of loops, and the most important thing- No conditions garbeach at all, the code just flow.

namespace code_kingdom {
    template <typename T>
    class land {
    public:
        land(T);
        T sum() { return std::accumulate(vec.begin(), vec.end(), T()); }
        
    private:
        std::vector<T> vec;
    };
}

You got payed, and went back to your place. The years goes by, and one day, and a strange bug occured: 1 + 2 + 3.6 + 1 = 123.61. The king order his best developer to solve the issue, and the developer, under the pressure successfully came with a solution:

namespace code_kingdom {
    template <typename T>
    double to_number(T num) {
        if constexpr (std::is_same_v<T, std::string>) return std::stod(num);
        else return std::stod(std::to_string(num));
    }

    template <typename T>
    class land {
    public:
        explicit land(T t) {
            vec.push_back(t);
        };

        void insert(T t) {
            vec.push_back(t);
        }

        T sum() {
            double res = 0;
            for (auto &elem : vec) {
                res += to_number(elem);
            }
            if constexpr (std::is_same_v<T, std::string>) {
                return std::to_string(res);
            } else {
                return res;
            }
        }

    private:
        std::vector<T> vec;
    };
}

Few weeks past, and the kingdom crashed, with a note:

error: could not convert ‘res’ from ‘double’ to ‘goblin’
  line |                 return res;
error: no matching function for call to ‘to_string(goblin&)’
  line |         else return std::stod(std::to_string(num));

The best coders in the kingdom worked for weeks on a solution, for the new goblins colony. When they finally came with a solution, the code was so big, that all of the developers left this kingdom, and split to different ways.

Today, the king keep looking for developers, who will maintain his kingdom as much as they can, and keep pay a lot of money for the best developers he can find.

Instructive Lesson

“code a single solution that will solve many problems” – This rule should be treated carefully. Few mistakes that won’t be handled by a software architect expert, can lead to another mistakes and to an architecture modifications, which in order to reverse them, you will lose more time than you can spent at the moment, and.. Done. You’ll have a broken architecture for life (and this is the optimistic vision).


Metaprogramming Restrictions

Before we design an architecture, we should know well the issue we aim to solve. Knowing the problem from every direction is the only way we can create a maintainable solution. In our kingdom, the software architect created an almost perfect architecture, because he didn’t cover up some unexpected cases- and he didn’t mentioned it.

namespace code_kingdom {
    /*
     * This class can handle numeric types only.
     * Any other type that would passed to this class might lead to an undefined behavior.
     */
    template <typename T>
    class land {
        /* ... */
    };
}

Well.. Good for you, no one will blame you for the kingdom fall, but it might still happen- Let’s save a kingdom.


#include <type_traites>

Before we’ll talk about the actual ways to code restrictions / validations on templates params, let’s inspect this standard library header. This header brought us some tools to help us to inspect template type parameters, and to gather information about them- at compile time.

How does it work?

A structure validates a condition using explicit specialization. Inside the explicit / partial specialization it declares a variable, that we are using for the inspection. One of the easiest structures is std::is_const:

template <typename>
struct is_const {
    static constexpr bool value = false;
};

template <typename _Tp>
struct is_const<_Tp const> {
    static constexpr bool value = true;
};

template <typename _Tp>
inline constexpr bool is_const_v = is_const<_Tp>::value; // Since C++17

// Usage: is_const<int>::value Or is_const_v<int>

This is an example of true/false value. Some of the tools actually help us to get a type:

template<bool _Cond, typename _Iftrue, typename _Iffalse>
struct conditional { typedef _Iftrue type; };

// Partial specialization for false.
template<typename _Iftrue, typename _Iffalse>
struct conditional<false, _Iftrue, _Iffalse> { typedef _Iffalse type; };

template<bool _Cond, typename _Iftrue, typename _Iffalse>
using conditional_t = typename conditional<_Cond, _Iftrue, _Iffalse>::type;

// Usage: conditional<condition, int, float>::type Or conditional_t<condition, int, float>

A little more complex example- If the specified condition is true, we won’t get into the partial specialization, and the declared type would be the first type we entered, otherwise we’ll get into the partial specialization, and the declared type would be the second type we entered.

Here is some tools we can use:

  • std::is_base_of<Base, Derived> – Check if Derived inherit from Base.
  • std::is_same<T, U> – Check if T == U.
  • std::is_convertible<From, To> – Check if From is convertible to To.
  • std::is_integral<T> – Check if T is an integral type.
  • std::is_floating_point<T> – Check if T is a floating point type.
  • std::is_arithmetic<T> – Check if T is an integral type or a floating-point type.

More tools can be found here: cppreference-types.


Static_Assert

Definition

This one is probably the most intuitive way of compile time type / value restrictions. static_assert declaration consists of two parts:

static_assert(/*bool_constexpr*/, /*message*/);
static_assert(/*bool_constexpr*/); // Since C++17, message is optional

bool_constexpr:

If bool_constexpr returns true, this declaration has no effect. Otherwise a compile-time error is issued, and the text of message, if any, is included in the diagnostic message.

cppreference.com/static_assert

message: A string literal.

Usage

Back to our story- The brilliant software architect heard about the kingdom and decided to go back in time (Yea, brilliant as I mentioned) and added a single line:

template <typename T>
class land {
    static_assert(std::is_arithmetic_v<T>, "land accepts arithmetic types only.");

public:
    /* ... */

    T sum() { return std::accumulate(vec.begin(), vec.end(), T()); }

private:
    std::vector<T> vec;
};

This line alone, won’t let the code compile as long as the type T is not an arithmetic type. Moreover it makes an announcement from the software architect, that this architecture can handle only an arithmetic types, and it does it on the most optimized way it can for those types. This way, when a different type would mistakenly pass to this class, a compilation error will occur. A possible message:

In instantiation of ‘class code_kingdom::land<goblin>’:
...
error: static assertion failed: land accepts arithmetic types only.
line | static_assert(std::is_arithmetic_v<T>, "land accepts arithmetic types only.");

Any developer who see this intuitive error, will understand that any modification in this architecture should be taken very carefully, and it might be a good idea to contact the software architect who built it before the modification.

satatic_assert can also appear inside functions and namespaces.


Decltype Return Value

For functions we can also use the following syntax, to make sure we get the types we know how to handle as template params:

template <typename T>
decltype(expression with T) func() {}

For example:

template <typename T>
decltype(T() + T()) func(T t) {
    return t + t;
}

Here we have two requirement:

  1. T have a default constructor.
  2. T should overload the operator+.

The same function could be written without the decltype section (using auto instead), but then we would get unclear error again:

error: no match for ‘operator+’ (operand types are ‘goblin’ and ‘goblin’)
line | return t + t;

With this part, we can understand that this function is only for types that match the restrictions:

error: no matching function for call to ‘func(goblin)’
line | func(goblin());
...
note: candidate: ‘template<class T> decltype ((T() + T())) func(T)’
line | decltype(T() + T()) func(T t) {
...
note:   template argument deduction/substitution failed:
line : In substitution of ‘template<class T> decltype ((T() + T())) func(T) [with T = goblin]’:
...
error: no match for ‘operator+’ (operand types are ‘goblin’ and ‘goblin’)
line | decltype(T() + T()) func(T t) {

Now let’s make a function that can handle goblin types too:

template <typename T>
decltype(T() + T()) func(T t) {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
    return t + t;
}

template <>
goblin func<goblin>(goblin t) { // Won't compile
    std::cout << __PRETTY_FUNCTION__ << std::endl;
    return t;
}

Seems cool, but this function is not a specialization of the first, due to a different return value requirements. Back to a generic solution:

template <typename T>
auto func(T t) {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
    return t;
}

Well, this one would work, at least until we’ll face a type that will mach both functions, and then we’ll get an ambitious compilation error:

error: call of overloaded ‘func(int)’ is ambiguous

To solve it, we’ll have to use the same decltype solution, to make sure that T is a goblin type:

template <typename T>
decltype(static_cast<goblin>(T())) func(T t) {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
    return t;
}

We can as well to do it without setting a default constructor requirement, but then we would need to use the t value instead of the T type, and it will look something like that:

template <typename T>
auto func(T t) -> decltype(static_cast<goblin>(t)) {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
    return t;
}

Attention! This restriction is simple, but it still won’t restrict std::string and other non-arithmetic types that overload operator+ and have a default constructor. In the next article I’ll show a decltype usage example that enable only arithmetic types.


Typename

This restriction can be apply anytime you use template.

Syntax

template <typename T, typename = std::enable_if_t<condition>>

Explanation

enable_if_t is one of <type_traits> header tools. To explain how this thing works, let’s look on its implementation:

template<bool, typename _Tp = void>
struct enable_if {};

// Partial specialization for true.
template<typename _Tp>
struct enable_if<true, _Tp> { typedef _Tp type; };

template<bool _Cond, typename _Tp = void>
using enable_if_t = typename enable_if<_Cond, _Tp>::type;

We are trying to access a type property that exists only in a partial specialization of enable_if, when the condition value is true. If the condition value is false, this is a substitution failure (we’ll discuss about this failure in a future article- for now just know that “Substitution failure is not an error”).

Story Example

template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>>
class land {
public:
    /* ... */

    T sum() { return std::accumulate(vec.begin(), vec.end(), T()); }

private:
    std::vector<T> vec;
};

This way, any type that is not an arithmetic type will cause a compile-time error.


Requires

Since C++20 we can use requires to specify restrictions. Before we go through the syntax, I want to inspect first the error (Since when I began to love them so much??):

Well, the first error to be printed out:

error: class template argument deduction failed:
line | code_kingdom::land l(g);

I like this one- It’s a clear clue about the reason, but keep reading the error, and we’ll see something new:

error: template constraint failure for ‘template<class T>  requires  is_arithmetic_v<T> class code_kingdom::land’
note: constraints not satisfied
  required by the constraints of ‘template<class T>  requires  is_arithmetic_v<T> class code_kingdom::land’
note: the expression ‘is_arithmetic_v<T> [with T = goblin]’ evaluated to ‘false’
line | requires (std::is_arithmetic_v<T>)

Admit it, to expect an error to be more understandable that that, is to expect it to solve your code (it might happen one day, give it some time). There is literally a line says: note: constraints not satisfied
required by the constraints of ‘template<class T> requires is_arithmetic_v class code_kingdom::land‘
`.

Syntax

Now that we know why we want to use this tool, let’s learn how to use it.

template <typename T>
    requires (expression) // #1
...

// Or

template <typename T>
    requires requires (params) { params-expressions } // #2
...

// Or [for functions only]

template <typename T>
void func(T t) requires (expression); // #3

#1/3 – expression: A boolean expression:

template <typename T>
    requires (std::is_arithmetic_v<T> && !std::is_const_v<T>)
auto func(T &t) {
    t++;
    return t + t;
}

int main() {
    int a = 5;
    std::cout << a << std::endl; // 5
    std::cout << func(a) << std::endl; // 12
    std::cout << a << std::endl; // 6
    return EXIT_SUCCESS;
}

#2 – params: Template types params.
#2 – params-expressions: Like inside a function.

template <typename T>
    requires requires (T type) {
        type + type; // type.operator+(type)
        type * type; // type.operator*(type)
        my_func(type); // function in this scope named my_func that accepts T
    }
auto func(T t) {
    return t + t * t;
}

int main() {
    func(5); // compile-time error
    return EXIT_SUCCESS;
}

The error:

...
note: constraints not satisfied
...
note: the required expression ‘my_func(type)’ is invalid
line | my_func(type); // function in this scope named my_func that accepts T

Concepts

Another feature that came to us in C++20 is the concept. Instead of using typename / class keywords inside a template expression, we can now specify named rules:

template <typename T>
concept LandType = requires() {
    std::is_arithmetic_v<T>;
};

template <LandType T>
class land { /* ... */ };

This tool make it possible to avoid code duplication in our restrictions. Assume we have another function, outside out land class, that should generate a land class inside:

template <LandType T>
land<T> generate_land() {
    return land<T>();
}

Conclusion

As developers who solve problems, we should always make sure that the code we write is clear not only to understand why is it working, but also to understand why it isn’t. If there are cases that we don’t mean our code to work for, we should make sure to make an announcement for it.

At the next part of this series we’ll talk about variadic template restrictions, and we’ll see how to apply them in C++11, 17, and 20.

Full examples repository: cppsenioreas-metaprogramming-restrictions

More posts in this series:

2 thoughts on “The Exact Solution for a Generic Problem – Part 1

Leave a comment