DEV Community

loading...

Forbid a particular specialization of a template

pgradot profile image Pierre Gradot Updated on ・3 min read

Let's imagine a simple template function that performs basic numerical computations:

template <typename T>
T f(T t) {
    return 2 * t + 3;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 👍🏻

Discussion (8)

pic
Editor guide
Collapse
atom3333 profile image
Thomas Ferrand

Small typo at the beginning

f(2.3) returns 7.6 and T is int
Should be double.

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 function f is callable with a bool. Indeed with the static_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 an int, the static_assert would fail to compile because the library would think that the function is callable with a bool and attempt to do so before encountering the static_assert. With the other 2 approach, the library would correctly detect that the function cannot be called with a bool and will instead call it with an int.

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).

Collapse
pgradot profile image
Pierre Gradot Author

Thanks, I have corrected this mistake.

call your function with a bool if supported, else call with an int

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?

Collapse
atom3333 profile image
Thomas Ferrand

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 my struct 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).

Collapse
sandordargo profile image
Sandor Dargo • Edited

Just a bit of nitpicking. =delete is available also since C++11 according to CppReference

Deleted functions

Regarding 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:

#include <concepts>
#include <iostream>

template <typename T>
concept Number = (std::integral<T> || std::floating_point<T>) && (not std::same_as<T, bool>);

template <typename T> 
requires Number<T>
T f(T t) {
    return 2 * t + 3;
}

template <typename T> 
auto f2(T t) requires Number<T> {
    return 2 * t + 3;
}

template <Number T>
auto f3(T t) {
    return 2 * t + 3;
}

auto f4(Number auto t) {
    return 2 * t + 3;
}

int main () {
    std::cout << f(2) << std::endl;
    std::cout << f2(2.2) << std::endl;
    std::cout << f3(3) << std::endl;
    std::cout << f4(-2.4) << std::endl;
    // std::cout << f2(true) << std::endl; // auto f2(T) requires  Number<T> [with T = bool]' with unsatisfied constraints
}
Enter fullscreen mode Exit fullscreen mode

I'll write a more detailed post about the 4 ways of concepts.

_Please note that the concept Number is incomplete, it accepts char for example _

Collapse
pgradot profile image
Pierre Gradot Author • Edited

This is not being "nitpicking": this is being "accurate". Thanks! I fixed that.

In fact, I missed this warning:

warning: deleted function definitions are a C++11 extension [-Wc++11-extensions]

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 ; )

Collapse
sandordargo profile image
Sandor Dargo

Gosh, I started that article, and it's already longer than the whole series on constness. I'll have to break it down :)

Thread Thread
pgradot profile image
Pierre Gradot Author

Not surprising XD

Collapse
sandordargo profile image
Sandor Dargo

Thanks a lot for the PS :)

I have to start writing that article soon!