Forbid a particular specialization of a template
Pierre Gradot

Pierre Gradot @pgradot

About: Committed defender of C++ / Learning Rust / Python & CMake lover / Black metal guitarist & mountain bike rider

Location:
Nantes, France
Joined:
Mar 25, 2020

Forbid a particular specialization of a template

Publish Date: Jan 11 '21
1 8

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

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

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

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 😉

Comments 8 total

  • Sandor Dargo
    Sandor DargoJan 11, 2021

    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 _

    • Pierre Gradot
      Pierre GradotJan 11, 2021

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

      • Sandor Dargo
        Sandor DargoJan 11, 2021

        Thanks a lot for the PS :)

        I have to start writing that article soon!

      • Sandor Dargo
        Sandor DargoFeb 1, 2021

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

  • Thomas Ferrand
    Thomas FerrandJan 11, 2021

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

    • Pierre Gradot
      Pierre GradotJan 11, 2021

      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?

      • Thomas Ferrand
        Thomas FerrandJan 11, 2021

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

Add comment