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 👍🏻
Top comments (8)
Small typo at the beginning
I also want to point out that the
static_assert
approach has a major drawback compared to the other ones. It prevents you to accurately detect if your functionf
is callable with abool
. Indeed with thestatic_assert
you 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
bool
if supported, else call with anint
, thestatic_assert
would fail to compile because the library would think that the function is callable with abool
and 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 abool
and 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_assert
is 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
struct
I can showcase what I was talking about (because now I can make a template that takes mystruct
as 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).
Just a bit of nitpicking.
=delete
is 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
Number
is incomplete, it acceptschar
for 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 ; )
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
Thanks a lot for the PS :)
I have to start writing that article soon!