There are a lot of ways to approach this, so let's compare them:
Constraints (since C++20)
#include <concepts>
// abbreviated function template, could also write:
// template <std::same_as<A> T>
void fn(std::same_as<A> auto arg) {
std::cout << "fn called";
}
Diagnostic
<source>:8:6: note: candidate template ignored: constraints not satisfied [with arg:auto = B]
void fn(std::same_as<A> auto arg) {
^
<source>:8:9: note: because 'std::same_as<B, A>' evaluated to false
void fn(std::same_as<A> auto arg) {
^
This option works because auto
is deduced to B
here, which is not the same as A
. The constraint doesn't care that B
is convertible to A
(although std::convertible_to
would).
Pros and Cons
- short and simple
- errors are slightly verbose by comparison
- requires fairly recent version of C++
- intent conveyed clearly in code
std::enable_if
and SFINAE (since C++11)
// note: convenience aliases is_same_v and enable_if_t are not available in C++11 yet
// this solution is C++17
template <typename T>
auto fn(T arg) -> std::enable_if_t<std::is_same_v<T, A>> {
std::cout << "fn called";
}
Diagnostic
<source>:9:6: note: candidate template ignored: requirement 'std::is_same_v<B, A>'
was not satisfied [with T = B]
auto fn(T arg) -> std::enable_if_t<std::is_same_v<T, A>> {
^
This implementation is basically the same as the C++20 version, we just do it via SFINAE instead of constraints.
Pros and Cons
- more verbose than C++20 counterpart, but bearable
- solution is much uglier in C++11 because there are no convenience aliases
- only clang has diagnostics this good for
std::enable_if
, other compilers will produce much worse output
- intent conveyed clearly like in the former solution
Deleted functions (since C++11)
void fn(A arg) {
std::cout << "fn called";
}
template <typename T>
void fn(T arg) = delete;
Diagnostic
<source>:14:6: note: candidate function [with T = B] has been explicitly deleted
void fn(T arg) = delete;
^
<source>:8:6: note: candidate function
void fn(A arg) {
^
We are allowed to declare any function as deleted, where calling it makes the program ill-formed.
This solution works because fn(T)
wins in overload resolution, as no implicit conversion from B
to T
is required, only from B
to A
.
Pros and Cons
- requires function overloading
= delete
on arbitrary functions is surprising, it's not a well-known feature
- intent not conveyed as clearly in code, we must look at the overload set as a whole to understand it
- no standard library dependency, i.e. good for compile speed potentially and very portable
static_assert
(since C++11)
template <typename T>
void fn(T arg) {
// or std::is_same_v in C++17
static_assert(std::is_same<T, A>::value, "fn must be called with A");
std::cout << "fn called";
}
Diagnostic
<source>:10:5: error: static assertion failed due to requirement 'std::is_same<B, A>::value':
fn must be called with A
static_assert(std::is_same<T, A>::value, "fn must be called with A");
^
This is a very simple, but effective solution.
We check whether the type we were given is actually A
. No implicit conversions from B
to A
would be considered.
Pros and Cons
- no error at the call site (bad IDE support, usually no red underline shown)
- very clear diagnostic, customizable by us so we can convey intent
Conclusion
The quality of errors is alright for every solution, at least when using clang.
Which solution you prefer depends in part on what requirements you have, and what version of C++ you want to be compatible with.
However, personal preference also plays a role here, since none of the solutions are better than others in every regard.