Search code examples
c++enumsc++20c++-concepts

Can I write a C++ concept that requires a function-template to which some enum value must be supplied as a template parameter?


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:

  1. The first case has the error Constraint variable 'n' cannot be used in an evaluated context.
  2. The second case compiles without any errors, but notifiable<notifiable_type, categories> isn't satisfied.

Can anyone clarify this for me? Is what I'm trying to write possible?


Solution

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