Search code examples
c++language-lawyerc++-concepts

C++ Concept to determine arity and type of functions


The following code intends to provide a concept for functions with a given signature and arity:

#include<utility>    //std::index_sequence
#include<cstddef>    //std::size_t
#include<functional> //std::invoke

struct A {};
struct B {};

auto f1 = [](auto) -> A { return {}; };  //should work also for generic arguments
auto f3 = [](A,A,A) -> A { return {}; };
auto g1 = [](B) -> B { return {}; };

//a unary A-function is a function A -> A
template<typename function_t>
concept is_unary_A_function = requires
{
    { std::invoke(std::declval<function_t>(), std::declval<A>()) } -> std::same_as<A>;
};

//an Nary A-function is a function with signature A x A x ... A (N-times) -> A
template<typename function_t, std::size_t N>
concept is_Nary_A_function = requires
{
    { []<size_t ... I>(auto function, auto a, std::index_sequence<I ...>)
        {
            return std::invoke(function, (I, a) ...);
        }(std::declval<function_t>(), std::declval<A>(), std::make_index_sequence<N>()) } -> std::same_as<A>;
};

static_assert(is_unary_A_function<decltype(f1)>);        //OK
static_assert(!is_unary_A_function<decltype(g1)>);       //OK
static_assert(!is_unary_A_function<decltype(f3)>);       //OK
static_assert(is_Nary_A_function<decltype(f3), 3>);      //OK
//static_assert(!is_Nary_A_function<decltype(f1), 3>);   //compile-time error

godbolt

In words, the is_unary_A_function concept should be true if it is inserted a function f1: A -> A, i.e. a function which takes a parameter of type A and returns a type A. The is_Nary_A_function with a given arity N concept should be true for a function fN : A x A x ... A -> A, that is a function which takes N parameters of type A and again returns an A.

Here, in the is_Nary_A_function concept, the usual approach using index_sequences is applied to call the provided function with N parameters of type A.

However, as soon as I evaluate the the concept is_Nary_A_function<decltype(f1), 3>, I get a compile-time error stating no matching function for call to 'invoke' which basically complains that the compiler can't call f1(a,a,a). However, the compiler also can't call g1(a), which is checked in the !is_unary_A_function<decltype(g1)> expression, but this seems to work correctly.

Thus, the lambda closure in the N-ary function concept seems to make a difference. Can someone please explain a reason for that and show a possible workaround?


Solution

  • By making your lambda SFINAE friendly, your code works:

    //an Nary A-function is a function with signature A x A x ... A (N-times) -> A
    template<typename function_t, std::size_t N>
    concept is_Nary_A_function = requires
    {
        { []<size_t ... I>(auto function, auto a, std::index_sequence<I ...>)
        -> decltype(std::invoke(function, (I, a) ...))
            {
                return std::invoke(function, (I, a) ...);
            }(std::declval<function_t>(), std::declval<A>(), std::make_index_sequence<N>()) } -> std::same_as<A>;
    };
    

    Demo.

    Note: requires has syntax to allow to avoid std::declval:

    //a unary A-function is a function A -> A
    template<typename function_t>
    concept is_unary_A_function = requires
    {
        { std::invoke(std::declval<function_t>(), std::declval<A>()) } -> std::same_as<A>;
    };
    

    can simply be

    //a unary A-function is a function A -> A
    template<typename function_t>
    concept is_unary_A_function = requires(function_t f, A a)
    {
        { std::invoke(f, a) } -> std::same_as<A>;
    };
    

    Demo