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

Is there a more ergonomic way of defining function parameter requirements in C++20 concepts?


With C++20 concepts, I've started using them as a way to define templated class "interfaces", as they make for very nice compiler errors. However, I'm struggling a little to define them in an ergonomic, and readable way.

For example, let's say I want to make a concept IFoo that statically checks a class has the following three functions with the exact input/return types:

struct Foo {
    void bar(uint32_t a, uint16_t b, uint8_t c) {}
    int32_t baz(int32_t d, int16_t e, uint8_t f) {return 0;}
    void bax(char g, uint8_t *h, uint32_t *i) {}
};

If this concept is defined correctly, then static_assert(IFoo<Foo>); would compile. The only way I can get this behavior in a "single" concept definition is as follows:

template<typename T>
concept IFoo = requires(T ifoo, uint32_t a, uint16_t b, uint8_t c, int32_t d, int16_t e, uint8_t f, char g, uint8_t *h, uint32_t *i) {
    {ifoo.bar(a, b, c)} -> std::same_as<void>;
    {ifoo.baz(d, e, f)} -> std::same_as<int32_t>;
    {ifoo.bax(g, h, i)} -> std::same_as<void>;
};

This satisfies the requirement, however it's not particularly readable as you have to mentally transfer the (many) types from the requires() arguments to the function inputs to form an idea of what the function prototypes should look like if you implement this class. Another way to accomplish this is defining a different concept per function and chaining them like so:

template<typename T>
concept IFoo_bar = requires(T ifoo, uint32_t a, uint16_t b, uint8_t c) {
    {ifoo.bar(a, b, c)} -> std::same_as<void>;
};

template<typename T>
concept IFoo_baz = requires(T ifoo, int32_t d, int16_t e, uint8_t f) {
    {ifoo.baz(d, e, f)} -> std::same_as<int32_t>;
};


template<typename T>
concept IFoo_bax = requires(T ifoo, char g, uint8_t *h, uint32_t *i) {
    {ifoo.bax(g, h, i)} -> std::same_as<void>;
};

template<typename T>
concept IFoo = requires { 
    requires IFoo_bar<T>;
    requires IFoo_baz<T>;
    requires IFoo_bax<T>;
};

However this creates a lot of boilerplate code which isn't ideal. What I really want (that doesn't compile) is the ability to nest requirement blocks like so:

template<typename T>
concept IFoo = requires(T ifoo) {
    requires(uint32_t a, uint16_t b, uint8_t c) {
        {ifoo.bar(a, b, c)} -> std::same_as<void>;
    };
    requires(int32_t d, int16_t e, uint8_t f) {
        {ifoo.baz(d, e, f)} -> std::same_as<int32_t>;
    };
    requires(char g, uint8_t *h, uint32_t *i) {
        {ifoo.bax(g, h, i)} -> std::same_as<void>;
    };
};

Am I missing something? Or are the avenues I've already explored the only way to accomplish what I'm after? Really am hoping I'm missing something in the docs that will make this a bit easier!


Solution

  • You don't need to name every single parameter in your requires clause.

    struct Foo {
      void bar(int n) {}
    };
    
    template<typename T>
    concept IFoo = requires(T foo) {
      {foo.bar(0)} -> std::same_as<void>;
    };
    
    

    works perfectly fine. If you have a type whose values are a bit more complicated to construct you can just use declval

    struct Foo {
      void bar(Unmentionable n) {}
    };
    
    template<typename T>
    concept IFoo = requires(T foo) {
      {foo.bar(std::declval<Unmentionable>())} -> std::same_as<void>;
    };
    

    the params in requires are all basically just shorthands for declval anyway, you don't need to write any at all:

    template<typename T>
    concept IFoo = requires {
      {std::declval<T>().bar(std::declval<Unmentionable>())} -> std::same_as<void>;
    };
    

    However, if you really like the requires syntax a lot you can actually nest them as in your original question:

    template<typename T>
    concept IFoo = requires(T ifoo) {
        requires requires(uint32_t a, uint16_t b, uint8_t c) {
            {ifoo.bar(a, b, c)} -> std::same_as<void>;
        };
        requires requires(int32_t d, int16_t e, uint8_t f) {
            {ifoo.baz(d, e, f)} -> std::same_as<int32_t>;
        };
        requires requires(char g, uint8_t *h, uint32_t *i) {
            {ifoo.bax(g, h, i)} -> std::same_as<void>;
        };
    };
    

    You just had one requires too few (the goofy double requires comes because one introduces a nested requirement, and a requires expression is a possible thing to put there; hence requires requires.