Let's imagine a simple template function that performs basic numerical computations:
template <typename T>
T f(T t) {
return 2 * t + 3;
}
Nothing seems wrong with this function. You can use it on several types: f(15)
returns 33 and T
is int
, f(2.3)
returns 7.6 and T
is double
, etc. If T
is not a type that supports addition and multiplication, you get a compiler error. Example with f("hello")
:
error: invalid operands to binary expression ('int' and 'const char *')
Nevertheless, you can run into cases that you had not planned. For instance, f(true)
is valid, doesn't raise any warning, and returns true
(in fact, it returns 5 converted to a Boolean, which is true
).
Let's try how we can reject f<bool>
at compile-time. We will see that the possibilities to forbid a particular specialization of a template have evolved with the versions of C++. For each technique, I will show the error my compiler, clang, generates for f(true)
.
Note that my purpose here is not to properly handle types that don't support addition and multiplication, just to forbid a particular specialization.
=delete on specialization
Since C++11
The first solution is to explicitly delete the specialization of f()
for T == bool
:
template<>
bool f(bool) = delete;
error: call to deleted function 'f'
note: candidate function [with T = bool] has been implicitly deleted
static_assert
The second solution is to add a static assertion on T
in f()
. static_assert
was introduced in C++11. The standard library has two techniques to check that T
is not bool
, one from C++11, the other from C++17.
Since C++11
C++11 introduced the template structure std::is_same
, which does exactly what you think it does.
#include <type_traits>
template <typename T>
T f(T t) {
static_assert(not std::is_same<T, bool>::value, "T cannot be bool");
return 2 * t + 3;
}
error: static_assert failed due to requirement '!std::is_same<bool, bool>::value' "T cannot be bool"
Since C++17
C++17 introduced the variable template std::is_same_v<U, V>
as a shortcut for std::is_same<U, V>::value
.
#include <type_traits>
template <typename T>
T f(T t) {
static_assert(not std::is_same_v<T, bool>, "T cannot be bool");
return 2 * t + 3;
}
error: static_assert failed due to requirement '!std::is_same_v<bool, bool>' "T cannot be bool"
Note: variable templates were introduced in C++14.
Concepts
Since C++20
Concepts are one of the biggest features of C++20. We have two possibilities with concepts to forbid T
from being bool
.
With a require clause
#include <concepts>
template <typename T> requires (not std::same_as<T, bool>)
T f(T t) {
return 2 * t + 3;
}
error: no matching function for call to 'f'
note: candidate template ignored: constraints not satisfied [with T = bool]
note: because '!std::same_as<_Bool, _Bool>' evaluated to false
With a custom concept
#include <concepts>
template <typename T, typename U>
concept different_than = not std::same_as<T, U>;
template <different_than<bool> T>
T f(T t) {
return 2 * t + 3;
}
error: no matching function for call to 'f'
note: candidate template ignored: constraints not satisfied [with T = bool]
note: because 'different_than<_Bool, _Bool>' evaluated to false
note: because '!std::same_as<_Bool, _Bool>' evaluated to false
Conclusion
In my opinion, =delete
has no advantages over the other techniques. I like static_assert
because you can write a custom error message, but at the end of the day, I believe concepts are greater because they have a clearer semantic. And that normal: they were made exactly to express constraints of template parameters.
👇🏻 Leave a comment to tell which technique you use/prefer and why 😃
PS: the idea of this article comes from the discussion on "Three ways to use the = delete specifier in C++" by Sandor Dargo 👍🏻