The Exact Solution for a Generic Problem – Part 2

After we proved that without restrictions a generic solution can be the reason for our architecture destruction, now it’s time to talk about variadic templates restrictions from C++11 to C++20.

Previous article in series: The Exact Solution for a Generic Problem – Part 1
Next article in series: Become a Compile-Time Coder

Why?

You got a task to create a function that sums all of it’s arguments. You remember you read two articles about variadic templates (Part 1, Part 2), and after few seconds you came up with a solution:

template <typename ...Args>
auto sum(Args ...args) {
    return (args + ...);
}

Everything works perfectly, you move your ticket to CR, and from there it is a short way to In Progress?! WHAT? In the explanation you got the following legal usage of your function:

sum(1, "123456", 2);

Which returns 456, the integers actually used as an offset for the char array:

const char *arr = "123456";
const char* sum(int arg0, const char *arg1, arg2) {
    return arg0 + arg1 + arg2; // arg1 with offset arg0 + arg2
}

To make sure that no one can do such mistakes, you have to use restrictions.


C++11/14

Typename restriction

Credit: https://stackoverflow.com/a/39659128/8038186

In C++17 we got a tool in the standard, we can use also in C++11/14 by copy-paste its implementation- conjunction:

template <bool... B>
struct conjunction {};

template <bool Head, bool... Tail>
struct conjunction<Head, Tail...> : std::integral_constant<bool, Head && conjunction<Tail...>::value>{};

template <bool B>
struct conjunction<B> : std::integral_constant<bool, B> {};

Now we can use this tool to apply the and operator between type conditions at compilation time:

template <typename T>
T sum(T t) {
    return t;
}

template <typename T, typename ...Args, typename = typename std::enable_if<conjunction<std::is_arithmetic<Args>::value...>::value>::type>
typename std::common_type<T, Args...>::type sum(T t, Args ...args) {
    return t + sum(args...);
}

Since C++14 we can simplify the return value to auto in both functions, and we can use enable_if_t instead of enable_if<...>::type [Note: We still have to define conjunction]:

template <typename T>
auto sum(T t) {
    return t;
}

template <typename T, typename ...Args, typename = typename std::enable_if_t<conjunction<std::is_arithmetic<Args>::value...>::value>>
auto sum(T t, Args ...args) {
    return t + sum(args...);
}

Static assertion restriction

We can clear the template line simply by moving the condition into a static assertion declaration.

template <typename T, typename ...Args>
auto sum(T t, Args ...args) { static_assert(conjunction<std::is_arithmetic<args>::value...>::value, "All parameters should be arithmetic params.");
    return t + sum(args...);
}

C++17

With fold-expressions and constexpr we got some significant simplifiers to our code, as we saw in previous articles. Let’s see the simplifying here:

Typename restriction

template <typename ...Args, typename = typename std::enable_if_t<(std::is_arithmetic_v<Args> && ...)>>
auto sum(Args ...args) {
    return (args + ...);
}

We can see here two major changes:

  • Get rid of std::conjunction with Fold-Expressions.
  • std::is_arithmethic_v instead of std::is_arithmethic<...>::value.

Static assertion restriction

template <typename ...Args>
auto sum(Args ...args) {
    static_assert((std::is_arithmetic_v<Args> && ...), "All parameters should be arithmetic params.");
    return (args + ...);
}

Decltype

template <typename ...Args>
decltype((Args() + ...)) sum(Args ...args) {
    return (args + ...);
}

Well, it’s definitely a restriction, and it’s compact & pretty, but is it enough? What about:

sum(std::string("Hell"), std::string("o "), "World")

Let’s go for another restriction:

template <typename ...Args>
typename std::enable_if_t<(std::is_arithmetic_v<Args> && ...), decltype((Args() + ...))> sum(Args ...args) {
    return (args + ...);
}

Looks complex- explanation:

typename std::enable_if_t – enable_if_t returns a type when the condition is true.

(std::is_arithmetic_v && …) – Our condition to enable only arithmetic types.

decltype((Args() + …) – The second enable_if_t template parameter is the return type if the condition is true. By default this type is void.

So- if the condition is true, we’ll get the type decltype((Args() + …), else we’ll get a compilation error.


C++20

Concepts & Requires make our code more readable and maintainable.

Requires

template <typename ...Args>
auto sum(Args ...args) requires ((std::is_arithmetic_v<Args> && ...)) {
    return (args + ...);
}

Concepts

My personal decision, that help in the fight to avoid code duplication [Sometimes we need to use the same restriction in different functions / classes]:

template <typename T>
concept Arithmetic = std::is_arithmetic_v<T>;

template <Arithmetic ...Args>
auto sum(Args ...args) {
    return (args + ...);
}

As simple as that – Both easy to read, and maintainable, thank you C++20!

Conclusion

It’s clear that every new C++ version brings us new important tools that help us to design & maintain our code.

In the next article we’ll dive deeper into compile-time computations with constexpr functions.

Full examples repository: cppsenioreas-metaprogramming-restrictions

More posts in this series:

Leave a comment