DEV Community

Pierre Gradot
Pierre Gradot

Posted on β€’ Edited on

1

Forbid a particular specialization of a template

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 πŸ‘πŸ»

AWS Security LIVE!

Tune in for AWS Security LIVE!

Join AWS Security LIVE! for expert insights and actionable tips to protect your organization and keep security teams prepared.

Learn More

Top comments (8)

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 β€’

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 β€’ β€’ 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 β€’

Not surprising XD

Collapse
 
sandordargo profile image
Sandor Dargo β€’

Thanks a lot for the PS :)

I have to start writing that article soon!

Sentry image

See why 4M developers consider Sentry, β€œnot bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

πŸ‘‹ Kindness is contagious

Explore a sea of insights with this enlightening post, highly esteemed within the nurturing DEV Community. Coders of all stripes are invited to participate and contribute to our shared knowledge.

Expressing gratitude with a simple "thank you" can make a big impact. Leave your thanks in the comments!

On DEV, exchanging ideas smooths our way and strengthens our community bonds. Found this useful? A quick note of thanks to the author can mean a lot.

Okay