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 see 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 requires 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 clearer semantics. 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 perhaps the most classical technique: using enable_if_t. BTW, I still believe that concepts are the best solution 😉