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 ofstd::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:
- C++ Meta Programming: Why?
- Basic templates usage – Part 1
- Basic templates usage – Part 2
- C++ templates – Beginners most common issue
- C++ – Partial / Explicit specialization
- Templates Infinity Theory – From C++11 to C++20 – Part 1
- Templates Infinity Theory – From C++11 to C++20 – Part 2
- The Exact Solution for a Generic Problem – Part 1
- The Exact Solution for a Generic Problem – Part 2
- Become a Compile-Time Coder
- Substitution Failure is Not an Error – SFINAE
- Template Base Class Collection