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
enable_if_t
Before concepts were added to the language, the classic solution was probably the infamous enable_if_t feature.
Since C++11
#include <type_traits>
template <typename T>
std::enable_if_t<not std::is_same<T, bool>::value, T>
f(T t) {
return 2 * t + 3;
}
Since C++17
As with previous techniques, std::is_same_v can be used:
#include <type_traits>
template <typename T>
std::enable_if_t<not std::is_same_v<T, bool>, T>
f(T t) {
return 2 * t + 3;
}
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 it's not SFINAE-friendly (see the comment below). At the end of the day, I believe concepts are greater because they have a clearer semantic. And that's 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 👍🏻
EDIT from June 2025: 4.5 years later, I realize that I forgot the perhaps most classical technique: using enable_if_t. BTW, I still believe that concepts are the best solution 😉
Top comments (8)
Just a bit of nitpicking.
=deleteis available also since C++11 according to CppReferenceRegarding concepts, it's worth to note (?) that in case you only want to accept numbers (I'm a bit vague) you have 4 different ways:
I'll write a more detailed post about the 4 ways of concepts.
_Please note that the concept
Numberis incomplete, it acceptscharfor example _This is not being "nitpicking": this is being "accurate". Thanks! I fixed that.
In fact, I missed this warning:
I have not really dived into concepts yet, so I am really interested by your future article!
PS: I have added a PS to my article above ; )
Thanks a lot for the PS :)
I have to start writing that article soon!
Gosh, I started that article, and it's already longer than the whole series on constness. I'll have to break it down :)
Not surprising XD
Small typo at the beginning
I also want to point out that the
static_assertapproach has a major drawback compared to the other ones. It prevents you to accurately detect if your functionfis callable with abool. Indeed with thestatic_assertyou have a function that is defined and takes part in overload resolution and simply produces an error when actually compiled.I you were using a library that, for some reason, wants to call your function with a
boolif supported, else call with anint, thestatic_assertwould fail to compile because the library would think that the function is callable with abooland attempt to do so before encountering thestatic_assert. With the other 2 approach, the library would correctly detect that the function cannot be called with abooland will instead call it with anint.Granted, this particular situation is unlikely to happen but there is a lot of similar situation in the standard library and in other library where something like that could occur. This is why
static_assertis not used much in the standard library but instead SFINAE is used (hopefully replaced by concepts in the future).Thanks, I have corrected this mistake.
I completely understand the principle. But it is not clear to me how would you write such a code. Can you provide a basic example please?
Hmm, I wasn't able to implement such detection for a free function because function template don't have a type (only concrete instantiation do).
By wrapping the function inside a
structI can showcase what I was talking about (because now I can make a template that takes mystructas a parameter).godbolt.org/z/z5a1h6
All in all maybe this is not really a problem with free function but only with member function (static or not).