I'd like to write a C++ concept that describes a type with a function-template that's specialised by an enum value. e.g.,
enum class categories {
t1,
t2
};
struct notifiable_type {
template<categories category>
auto notification() {}
};
auto main() -> int {
notifiable_type().notification<categories::t1>();
}
So far, I've tried:
// 1
template<typename notifiable_t, typename notification_t>
concept notifiable = std::is_enum<notification_t>::value && requires(notifiable_t t, const notification_t n) {
{ t.template notification<n>() } -> std::same_as<void>;
};
// 2
template<typename notifiable_t, typename notification_t>
concept notifiable = std::is_enum<notification_t>::value && requires(notifiable_t t) {
{ t.template notification<std::declval<notification_t>()>() } -> std::same_as<void>;
};
However, neither of these seem to work in the desired way:
Constraint variable 'n' cannot be used in an evaluated context
.notifiable<notifiable_type, categories>
isn't satisfied.Can anyone clarify this for me? Is what I'm trying to write possible?
You're pretty close with this one (I went ahead and changed std::is_enum<E>::value
to std::is_enum_v<E>
, and also reduced the names of the types since they're quite long and nearly identical to each other which confused me for a bit):
template<typename T, typename E>
concept notifiable =
std::is_enum_v<E>
&& requires(T t, const E n) {
{ t.template notification<n>() } -> std::same_as<void>;
};
The problem is you can't use n
like that because it needs to be a constant, and it's not. So the solution is to instead of inventing a variable of type E
to instead invent a value of type E
. And, well, 0
is as good a value as any:
template <typename T, typename E>
concept notifiable = std::is_enum_v<E>
&& requires(T t) {
{ t.template notification<E{}>() } -> std::same_as<void>;
};
Since E
is an enum, we know E{}
is valid. No need for declval
, which likewise has the same problem as your original approach: it's not a constant and it needs to be.
A similar, though much more verbose and generally weirder, solution would be:
template <typename T, typename E>
concept notifiable = std::is_enum_v<E>
&& requires(T t, std::integral_constant<E, E{}> n) {
{ t.template notification<n>() } -> std::same_as<void>;
};
This, now, works because integral_constant<T, V>
is constexpr-convertible to a T
with value V
. That conversion is extremely useful in a lot of contexts, since it allows something approximating passing constants as function parameters in a way that preserves their constant-ness.
But in this case it's pointless, so I'm just presenting it for illustration.
Note that this is still limited, we're only checking that specifically E{0}
works - not that any E
works (which we can't in general do). That's likely good enough, although you could get false negatives if, say, you have some situation where T::notification
has extra constraints on its E
.