2
\$\begingroup\$

Goal: implement traits that for anything callable return its arity, return type and the argument types. Since pointers to data members are also callable, those should be handled (and be considered 0-arity).

Code below, try here (EDIT: updated with new HasCallOperator concept that now works on MSVC, GCC and clang, based on this). This is v5 (v4 here), incorporating comments received on v4 and further enhancements:

  1. Can now handle functors (including lambdas) with overloaded/templated operator() by user providing expected argument types of the callable, with some documented limitations. Significantly, when user provides argument types that do not match in const or reference qualification, a fallback search is performed with all possible permutations of type qualifications to see if a match can be found. I think this is as far as this can be pushed without reflection. See here for a (much simpler) version without the fallback search mechanism, should you be interested.
  2. When an error occurs (e.g. "not a callable type"), this is now communicated as a property instead of through a static_assert, so that the library can be used to inspect unsanitized callables without triggering mandatory errors. static_asserts turning the error into a compile failure with an informative error message can be issued with a helper.
#pragma once

#include <cstddef>
#include <tuple>
#include <type_traits>

// inspired by https://github.com/kennytm/utils/blob/master/traits.hpp
// and https://stackoverflow.com/a/28213747. Further thanks to G. Sliepen
// on StackExchange CodeReview for numerous helpful suggestions.
// 
// invocable_traits inspects a callable and allows to query information about
// its return type, host class (if member function or data member, this
// includes lambdas), arity, argument types, and several properties of the
// function declaration. Specifically, the following properties are exposed:
// arity            : std::size_t value indicating the arity of the callable
//                    (not counting variadic input arguments, so int(int,...)
//                    has arity 1)
// is_const         : true if the callable is declared const
// is_volatile      : true if the callable is declared volatile
// is_noexcept      : true if the callable is declared noexcept
// is_variadic      : true if the callable has an old-/C-style variadic
//                    argument
// declared_result_t: return type as declared in the callable signature
// invoke_result_t  : return type when the callable is std::invoke()d
// class_t          : class this callable is a member of, void if free
//                    function
// arg_t            : indexable list of the declared argument types of
//                    the callable (e.g. use arg_t<0> to retrieve the
//                    first argument type)
// error            : if an error occurs (e.g. provided type is not callable)
//                    that is communicated through this property. You can
//                    issue an appropriate static_assert using the following:
//                    // example that will fail because int is not callable:
//                    using traits = invocable_traits::get<int>;
//                    invocable_traits::issue_error<traits::error>();
//                    Upon error, all other properties are set to void or
//                    false, as appropriate.
//
// When the user has provided argument types and they are used (see below),
// three additional properties are exposed:
// num_matched_overloads : number of overloads found that matched the
//                         user's provided argument types.
// is_exact_match        : true if the user-provided argument types
//                         were an exact match to an overload, false
//                         if the overload was found through the search
//                         procedure (see 2 below).
// matched_overload      : indexable list of matched overloads, each
//                         containing all the info about the matched
//                         callable, as described above. Use, e.g.
//                         matched_overload<0>::arg_t<0> to query the
//                         declared type of the first argument of the
//                         first found overload.
// 
// Note that this machinery does not handle overloaded or templated functions
// (but see below for the special case of overloaded operator()). It could
// not possibly do so, since these do not have a unique address. If references
// or pointers to specific overloads or template instantiations are passed,
// all works as expected.
// 
// To handle overloaded or templated operator() of functors (this includes
// lambdas), you can help invocable_traits find the right overload by
// providing additional type arguments, e.g.,
// invocable_traits::get<Functor, int>. If required for disambiguation (i.e.
// if the passed callable is indeed a functor with an overloaded or templated
// operator()), these extra type arguments are used to resolve the desired
// overload. If the additional type arguments are not required to resolve the
// overload, or a callable other than a functor is passed, the additional
// type arguments are ignored. It is thus not an error to provide the
// additional type arguments (e.g. if you know what input arguments your
// callable should take), but the callable will not be checked for a matching
// signature unless the additional type arguments were required for overload
// resolution/template instantiation. Use of std::is_invocable<> is therefore
// adviced in all cases where you provide additional type arguments to
// invocable_traits::get<>.
// 
// When resolving the overload using these extra type arguments, two things
// may happen:
// 1. the user provides the exact argument types (including const and
//    reference qualifiers) of an existing overload. In that case,
//    invocable_traits for only that overload are returned.
// 2. the user provides argument types that do not exactly match an
//    existing overload. In that case, invocable_traits generates all
//    possible const and reference qualified versions of the provided
//    argument types, and tests for overloads with all their permutations.
//    If any are found, invocable_traits for the first found overload are
//    returned, and invocable_traits for any additional matching overloads
//    can also be retrieved (see propeties above).
// When both procedures fail to yield a matching overload, an
// invocable_traits::error is set.
// 
// Note that even though std::is_invocable<> may yield true, there are
// various situations where invocable_traits::get<> will fail to find the
// right overload when provided with the same type arguments as
// std::is_invocable<>. These include at least:
// - implicit conversions of the argument type
// - old-/C-style variadic functions
// - default arguments
// To be able to find an overload, the correct type (excluding qualifiers,
// see procedure 2 above) of all input arguments (also default arguments)
// must be specified, and variadic inputs should not be specified (the
// presence of these will be deduced by invocable_traits and signalled
// through invocable_traits::is_variadic = true).

namespace invocable_traits
{
enum class Error
{
    None,
    NotAClass,
    NoCallOperator,
    IsOverloadedTemplated,
    OverloadNotResolved,
    Unknown
};

template <Error E>
void issue_error()
{
    static_assert(E != Error::NotAClass,
        "passed type is not a class, and thus cannot have an operator()");
    static_assert(E != Error::NoCallOperator,
        "passed type is a class that doesn't have an operator()");
    static_assert(E != Error::IsOverloadedTemplated,
        "passed passed type is a class that has an overloaded or templated operator(), specify argument types in invocable_traits invocation to disambiguate the operator() signature you wish to use");
    static_assert(E != Error::OverloadNotResolved,
        "passed type is a class that doesn't have an operator() that declares the specified argument types, or some const/ref-qualified combination of the specified argument types");
    static_assert(E != Error::Unknown,
        "an unknown error occurred");
};

namespace detail
{
    template <bool, std::size_t i, typename... Args>
    struct invocable_traits_arg_impl
    {
        using type = std::tuple_element_t<i, std::tuple<Args...>>;
    };
    template <std::size_t i, typename... Args>
    struct invocable_traits_arg_impl<false, i, Args...>
    {
        static_assert(i < sizeof...(Args), "Argument index out of bounds (queried callable does not declare this many arguments)");

        // to reduce excessive compiler error output
        using type = void;
    };

    template <
        typename Rd, typename Ri, typename C,
        bool IsConst, bool isVolatile, bool isNoexcept, bool IsVariadic,
        typename... Args>
    struct invocable_traits_class
    {
        static constexpr std::size_t arity = sizeof...(Args);
        static constexpr auto is_const    = IsConst;
        static constexpr auto is_volatile = isVolatile;
        static constexpr auto is_noexcept = isNoexcept;
        static constexpr auto is_variadic = IsVariadic;

        using declared_result_t = Rd;   // return type as declared in function
        using invoke_result_t   = Ri;   // return type of std::invoke() expression
        using class_t           = C;

        template <std::size_t i>
        using arg_t = typename invocable_traits_arg_impl<i < sizeof...(Args), i, Args...>::type;

        static constexpr Error error      = Error::None;
    };

    template <
        typename Rd, typename Ri,
        bool IsConst, bool isVolatile, bool isNoexcept, bool IsVariadic,
        typename... Args>
    struct invocable_traits_free : public invocable_traits_class<Rd, Ri, void, IsConst, isVolatile, isNoexcept, IsVariadic, Args...> {};


    // machinery to extract exact function signature and qualifications
    template <typename, typename...>
    struct invocable_traits_impl;

    // pointers to data members
    template <typename C, typename R>
    struct invocable_traits_impl<R C::*>
        : public invocable_traits_class<R,
                                        std::invoke_result_t<R C::*, C>,
                                        C,
                                        false, false, false, false
                                       > {};

    // pointers to functions
    template <typename R, typename... Args>
    struct invocable_traits_impl<R(*)(Args...)>                 : public invocable_traits_impl<R(Args...)> {};
    template <typename R, typename... Args>
    struct invocable_traits_impl<R(*)(Args...) noexcept>        : public invocable_traits_impl<R(Args...) noexcept> {};
    template <typename R, typename... Args>
    struct invocable_traits_impl<R(*)(Args..., ...)>            : public invocable_traits_impl<R(Args..., ...)> {};
    template <typename R, typename... Args>
    struct invocable_traits_impl<R(*)(Args..., ...) noexcept>   : public invocable_traits_impl<R(Args..., ...) noexcept> {};

    template <typename...>
    struct typelist {};

#   define IS_NONEMPTY(...) 0 __VA_OPT__(+1)
#   define MAKE_CONST(...)    __VA_OPT__(const)
#   define MAKE_VOLATILE(...) __VA_OPT__(volatile)
#   define MAKE_NOEXCEPT(...) __VA_OPT__(noexcept)
#   define MAKE_VARIADIC(...) __VA_OPT__(, ...)

    // functions, pointers to member functions and machinery to select a specific overloaded operator()
#   define INVOCABLE_TRAITS_SPEC(c,vo,e,va)                                             \
    /* functions */                                                                     \
    template <typename R, typename... Args>                                             \
    struct invocable_traits_impl<R(Args... MAKE_VARIADIC(va))                           \
                                 MAKE_CONST(c) MAKE_VOLATILE(vo) MAKE_NOEXCEPT(e)>      \
        : public invocable_traits_free<                                                 \
            R,                                                                          \
            std::invoke_result_t<R(Args... MAKE_VARIADIC(va))                           \
                                 MAKE_CONST(c) MAKE_VOLATILE(vo) MAKE_NOEXCEPT(e), Args...>,    \
            IS_NONEMPTY(c),                                                             \
            IS_NONEMPTY(vo),                                                            \
            IS_NONEMPTY(e),                                                             \
            IS_NONEMPTY(va),                                                            \
            Args...> {};                                                                \
    /* pointers to member functions */                                                  \
    template <typename C, typename R, typename... Args>                                 \
    struct invocable_traits_impl<R(C::*)(Args... MAKE_VARIADIC(va))                     \
                                 MAKE_CONST(c) MAKE_VOLATILE(vo) MAKE_NOEXCEPT(e)>      \
        : public invocable_traits_class<                                                \
            R,                                                                          \
            std::invoke_result_t<R(C::*)(Args...MAKE_VARIADIC(va))                      \
                                 MAKE_CONST(c) MAKE_VOLATILE(vo) MAKE_NOEXCEPT(e), C, Args...>, \
            C,                                                                          \
            IS_NONEMPTY(c),                                                             \
            IS_NONEMPTY(vo),                                                            \
            IS_NONEMPTY(e),                                                             \
            IS_NONEMPTY(va),                                                            \
            Args...> {};                                                                \
    /* machinery to select a specific overloaded operator() */                          \
    template <typename C, typename... OverloadArgs>                                     \
    auto invocable_traits_resolve_overload(std::invoke_result_t<C, OverloadArgs...>         \
                                           (C::*func)(OverloadArgs... MAKE_VARIADIC(va))    \
                                           MAKE_CONST(c) MAKE_VOLATILE(vo) MAKE_NOEXCEPT(e),\
                                           typelist<OverloadArgs...>)\
    { return func; };

    // cover all const, volatile and noexcept permutations
    INVOCABLE_TRAITS_SPEC( ,  , ,  )
    INVOCABLE_TRAITS_SPEC( ,Va, ,  )
    INVOCABLE_TRAITS_SPEC( ,  ,E,  )
    INVOCABLE_TRAITS_SPEC( ,Va,E,  )
    INVOCABLE_TRAITS_SPEC( ,  , ,Vo)
    INVOCABLE_TRAITS_SPEC( ,Va, ,Vo)
    INVOCABLE_TRAITS_SPEC( ,  ,E,Vo)
    INVOCABLE_TRAITS_SPEC( ,Va,E,Vo)
    INVOCABLE_TRAITS_SPEC(C,  , ,  )
    INVOCABLE_TRAITS_SPEC(C,Va, ,  )
    INVOCABLE_TRAITS_SPEC(C,  ,E,  )
    INVOCABLE_TRAITS_SPEC(C,Va,E,  )
    INVOCABLE_TRAITS_SPEC(C,  , ,Vo)
    INVOCABLE_TRAITS_SPEC(C,Va, ,Vo)
    INVOCABLE_TRAITS_SPEC(C,  ,E,Vo)
    INVOCABLE_TRAITS_SPEC(C,Va,E,Vo)
    // clean up
#   undef INVOCABLE_TRAITS_SPEC
#   undef IS_NONEMPTY
#   undef MAKE_CONST
#   undef MAKE_VOLATILE
#   undef MAKE_NOEXCEPT
#   undef MAKE_VARIADIC

    // check if passed type has an operator(), can be true for struct/class, includes lambdas
    // from https://stackoverflow.com/a/70699109/3103767
    // logic: test if &Tester<C>::operator() is a valid expression. It is only valid if
    // C did not have an operator(), because else &Tester<C>::operator() would be ambiguous
    // and thus invalid. To test if C has an operator(), we just check that
    // &Tester<C>::operator() fails, which implies that C has an operator() declared.
    // This trick does not work for final classes, at least detect non-overloaded
    // operator() for those.
    struct Fake { void operator()(); };
    template <typename T> struct Tester : T, Fake { };

    template <typename C>
    concept HasCallOperator = std::is_class_v<C> and (
        requires(C)                 // checks if non-overloaded operator() exists
        {
            &C::operator();
        } or
        not requires(Tester<C>)     // checks overloaded/templated operator(), but doesn't work on final classes
        {
            &Tester<C>::operator();
        }
    );
    // check if we can get operator().
    // If it fails (and assuming above HasCallOperator does pass),
    // this is because the operator is overloaded or templated
    // NB: can't simply do requires(C t){ &C::operator(); } because
    // that is too lenient on clang: it also succeeds for
    // overloaded/templated operator() where it shouldn't
    template <typename T>
    concept CanGetCallOperator = requires
    {
        invocable_traits_impl<
            decltype(
                &T::operator()
            )>();
    };
    // check if we can get an operator() that takes the specified arguments types.
    // If it fails (and assuming above HasCallOperator does pass),
    // an operator() with this specified cv- and ref-qualified argument types does
    // not exist.
    template <typename C, typename... OverloadArgs>
    concept HasSpecificCallOperator = requires
    {
        invocable_traits_resolve_overload<C>(
            &C::operator(),
            typelist<OverloadArgs...>{}
        );
    };
    namespace try_harder
    {
        // cartesian product of type lists, from https://stackoverflow.com/a/59422700/3103767
        template <typename... Ts>
        typelist<typelist<Ts>...> layered(typelist<Ts...>);

        template <typename... Ts, typename... Us>
        auto operator+(typelist<Ts...>, typelist<Us...>)
            ->typelist<Ts..., Us...>;

        template <typename T, typename... Us>
        auto operator*(typelist<T>, typelist<Us...>)
            ->typelist<decltype(T{} + Us{})...>;

        template <typename... Ts, typename TL>
        auto operator^(typelist<Ts...>, TL tl)
            -> decltype(((typelist<Ts>{} *tl) + ...));

        template <typename... TLs>
        using product_t = decltype((layered(TLs{}) ^ ...));

        // adapter to make cartesian product of a list of typelists
        template <typename... Ts>
        auto list_product(typelist<Ts...>)
            ->product_t<Ts...>;

        template <typename T>
        using list_product_t = decltype(list_product(T{}));

        // code to turn input argument type T into all possible const/ref-qualified versions
        template <typename T> struct type_maker_impl;
        // T* -> T*, T* const
        template <typename T>
            requires std::is_pointer_v<T>
        struct type_maker_impl<T>
        {
            using type = typelist<T, const T>;
        };
        // T -> T, T&, const T&, T&&, const T&& (NB: const on const T is ignored, so that type is not included)
        template <typename T>
            requires (!std::is_pointer_v<T>)
        struct type_maker_impl<T>
        {
            using type = typelist<T, T&, const T&, T&&, const T&&>;
        };

        template <typename T>
        struct type_maker : type_maker_impl<std::remove_cvref_t<T>> {};

        template <typename T>
        using type_maker_t = typename type_maker<T>::type;

        template <typename ...Ts>
        struct type_maker_for_typelist
        {
            using type = typelist<type_maker_t<Ts>...>;
        };

        // code to filter out combinations of qualified input arguments that do not
        // resolve to a declared overload
        template <typename, typename> struct concat;
        template <typename T, typename ...Args>
        struct concat<T, typelist<Args...>>
        {
            using type = typelist<T, Args...>;
        };

        template <typename...> struct check;
        template <typename C, typename... Args>
        struct check<C, typelist<Args...>>
        {
            static constexpr bool value = HasSpecificCallOperator<C, Args...>;
        };

        template <typename...> struct filter;
        template <typename C>
        struct filter<C, typelist<>>
        {
            using type = typelist<>;
        };
        template <typename C, typename Head, typename ...Tail>
        struct filter<C, typelist<Head, Tail...>>
        {
            using type = std::conditional_t<check<C, Head>::value,
                typename concat<Head, typename filter<C, typelist<Tail...>>::type>::type,
                typename filter<C, typelist<Tail...>>::type
            >;
        };

        // extract first element from a typelist
        template <typename, typename...> struct get_head;
        template <typename Head, typename ...Tail>
        struct get_head<typelist<Head, Tail...>>
        {
            using type = Head;
        };
    }

    // to reduce excessive compiler error output
    struct invocable_traits_error
    {
        static constexpr std::size_t arity       = 0;
        static constexpr auto        is_const    = false;
        static constexpr auto        is_volatile = false;
        static constexpr auto        is_noexcept = false;
        static constexpr auto        is_variadic = false;
        using declared_result_t                  = void;
        using invoke_result_t                    = void;
        using class_t                            = void;
        template <size_t i>
        using arg_t                              = void;
    };
    struct invocable_traits_error_overload
    {
        static constexpr std::size_t num_matched_overloads = 0;
        static constexpr auto        is_exact_match        = false;
        template <size_t i>
        using matched_overload                             = invocable_traits_error;
    };

    template <typename T, bool hasOverloadArgs>
    constexpr Error get_error()
    {
        if constexpr (!std::is_class_v<T>)
            return Error::NotAClass;
        else if constexpr (!HasCallOperator<T>)
            return Error::NoCallOperator;
        else if constexpr (hasOverloadArgs)
            return Error::OverloadNotResolved;
        else if constexpr (!hasOverloadArgs && !CanGetCallOperator<T>)
            return Error::IsOverloadedTemplated;

        return Error::Unknown;
    }

    template <typename C, typename Head>
    struct get_overload_info
    {
        using type =
            invocable_traits_impl<
                std::decay_t<
                    decltype(
                        invocable_traits_resolve_overload<C>(
                            &C::operator(),
                            Head{}
                        )
                    )
                >
            > ;
    };
    template <typename C, typename Head>
    using get_overload_info_t = typename get_overload_info<C, Head>::type;

    template <bool, std::size_t i, typename C, typename... Args>
    struct invocable_traits_overload_info_impl
    {
        using type = get_overload_info_t<C, std::tuple_element_t<i, std::tuple<Args...>>>;
    };
    template <std::size_t i, typename C, typename... Args>
    struct invocable_traits_overload_info_impl<false, i, C, Args...>
    {
        static_assert(i < sizeof...(Args), "Argument index out of bounds (queried callable does not have this many matching overloads)");

        // to reduce excessive compiler error output
        using type = void;
    };

    template <typename C, bool B, typename... Args> struct invocable_traits_overload_info;
    template <typename C, bool B, typename... Args>
    struct invocable_traits_overload_info<C, B, typelist<Args...>>
    {
        static constexpr std::size_t num_matched_overloads  = sizeof...(Args);
        static constexpr auto        is_exact_match         = B;

        template <std::size_t i>
        using matched_overload = typename invocable_traits_overload_info_impl<
            i < sizeof...(Args),
            i,
            C,
            Args...
        >::type;
    };

    // found at least one overload taking a const/ref qualified version of the specified argument types
    // that is different from those provided by the library user
    template <typename C, typename List>
    struct invocable_traits_extract_try_harder :
        invocable_traits_impl<              // instantiate for the first matched overload
            std::decay_t<
                decltype(
                    invocable_traits_resolve_overload<C>(
                        &C::operator(),
                        typename try_harder::get_head<List>::type{}
                    )
                )
            >
        > ,
        invocable_traits_overload_info<     // but expose all matched overloads
            C,
            false,
            List
        >
    {};

    // failed to find any overload taking the specified argument types or some const/ref qualified version of them
    template <typename T>
    struct invocable_traits_extract_try_harder<T, typelist<>> : // empty list -> no combination of arguments matched an overload
        invocable_traits_error,
        invocable_traits_error_overload
    {
        static constexpr Error error = get_error<T, true>();
    };

    // specific overloaded operator() is available, use it for analysis
    template <typename C, bool, typename... OverloadArgs>
    struct invocable_traits_extract :
        invocable_traits_impl<
            std::decay_t<
                decltype(
                    invocable_traits_resolve_overload<C>(
                        &C::operator(),
                        typelist<OverloadArgs...>{}
                    )
                )
            >
        >,
        invocable_traits_overload_info<
            C,
            true,
            typelist<typelist<OverloadArgs...>> // expose matched overload through this interface also, for consistency, even though matching procedure was not run
        >
    {};

    // unambiguous operator() is available, use it for analysis
    template <typename C, bool B>
    struct invocable_traits_extract<C, B> :
        invocable_traits_impl<
            decltype(
                &C::operator()
            )
        > {};

    // no specific overloaded operator() taking the specified arguments is available, try harder
    // to see if one can be found that takes some other const/reference qualified version of the
    // input arguments.
    template <typename T, typename... OverloadArgs>
    struct invocable_traits_extract<T, false, OverloadArgs...> :
        invocable_traits_extract_try_harder<
            T,
            typename try_harder::filter<T,                          // filter list of all argument combinations: leave only resolvable overloads
                try_harder::list_product_t<                         // cartesian product of these lists
                    typename try_harder::type_maker_for_typelist<   // produce list with all const/ref combinations of each argument
                        OverloadArgs...
                    >::type
                >
            >::type
        > {};

    template <typename T>
    struct invocable_traits_extract<T, false> : invocable_traits_error
    {
        static constexpr Error error = get_error<T, false>();
    };

    // catch all that doesn't match the various function signatures above
    // If T has an operator(), we go with that. Else, issue error message.
    template <typename T>
    struct invocable_traits_impl<T> :
        invocable_traits_extract<
            T,
            HasCallOperator<T>&& CanGetCallOperator<T>
        > {};

    // if overload argument types are provided and needed, use them
    template <typename T, bool B, typename... OverloadArgs>
    struct invocable_traits_overload_impl :
        invocable_traits_extract<
            T, 
            HasCallOperator<T> && HasSpecificCallOperator<T, OverloadArgs...>,
            OverloadArgs...
        > {};

    // if they are provided but not needed, ignore them
    template <typename T, typename... OverloadArgs>
    struct invocable_traits_overload_impl<T, false, OverloadArgs...> :
        invocable_traits_impl<
            T
        > {};
}

template <typename T, typename... OverloadArgs>
struct get :
    detail::invocable_traits_overload_impl<
        std::remove_reference_t<T>,
        detail::HasCallOperator<std::decay_t<T>> && !detail::CanGetCallOperator<std::decay_t<T>>,
        OverloadArgs...
    > {};

template <typename T, typename... OverloadArgs>
struct get<std::reference_wrapper<T>, OverloadArgs...> :
    detail::invocable_traits_overload_impl<
        std::remove_reference_t<T>,
        detail::HasCallOperator<std::decay_t<T>> && !detail::CanGetCallOperator<std::decay_t<T>>,
        OverloadArgs...
    > {};

template <typename T>
struct get<T> :
    detail::invocable_traits_impl<
        std::decay_t<T>
    > {};

template <typename T>
struct get<std::reference_wrapper<T>> :
    detail::invocable_traits_impl<
        std::decay_t<T>
    > {};
}

testing code:

#include <functional>
void test(const int)
{}
void test2(int) noexcept
{}
void testEllipsis(int,...)
{}

void testOver(int,double)
{}
void testOver(char,short)
{}

template <typename T>
std::conditional_t<std::is_floating_point_v<T>, double, int64_t> testTemplated(T a, char b)
{ return 1; }


struct tester
{
    int yolo(char) const
    { return 1; }
    void yoloEllipsis(char, ...) noexcept
    {}

    static long yoloStatic(short)
    { return 1; }

    void operator()(int in_) {}

    const int field;
};

template <typename T>
struct testerTemplated
{
    template <typename R>
    int yolo(char, R) const
    {return 3;}
    template <typename R>
    short yolo(double, R, ...)
    {return 4;}
    void yoloEllipsis(char, ...) noexcept
    {}

    template <typename R>
    static long yoloStatic(R)
    { return 1; }

    void operator()(int in_) {}

    const int field;
};

struct functorOverloaded
{
    int operator()(const int& in_, ...) const { return 1; }
    int operator()(int&& in_, ...) const { return 1; }
    int operator()(short in_, int&&, const char&) { return 1; }
    int operator()(const tester) { return 1; }
};

struct functorTemplated
{
    template <typename... T>
    int operator()(const T&... in_, ...) const
    { return 1; }
};


int main()
{
    auto lamb = [](const int& in_) {return "ret"; };

    using type1 = decltype(lamb);
    using traits1 = invocable_traits::get<type1>;
    invocable_traits::issue_error<traits1::error>();
    static_assert(std::is_same_v<const char*, traits1::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type1, int>, traits1::invoke_result_t>, "");
    static_assert(std::is_same_v<traits1::invoke_result_t, traits1::declared_result_t>, "");
    static_assert(std::is_same_v<decltype(lamb), traits1::class_t>, "");
    static_assert(std::is_same_v<const int&, traits1::arg_t<0>>, "");
    static_assert(traits1::is_const, "");

    using type1b = decltype(+lamb);
    using traits1b = invocable_traits::get<type1b>;
    invocable_traits::issue_error<traits1b::error>();
    static_assert(std::is_same_v<const char*, traits1b::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type1b, int>, traits1b::invoke_result_t>, "");
    static_assert(std::is_same_v<traits1b::invoke_result_t, traits1b::declared_result_t>, "");
    static_assert(std::is_same_v<void, traits1b::class_t>, "");
    static_assert(std::is_same_v<const int&, traits1b::arg_t<0>>, "");
    static_assert(!traits1b::is_const, "");


    using type2 = decltype(&test);
    using traits2 = invocable_traits::get<type2, int>;
    invocable_traits::issue_error<traits2::error>();
    static_assert(std::is_same_v<void, traits2::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type2, int>, traits2::invoke_result_t>, "");
    static_assert(std::is_same_v<traits2::invoke_result_t, traits2::declared_result_t>, "");
    static_assert(std::is_same_v<int, traits2::arg_t<0>>, "");

    using type2b = decltype(&test2);
    using traits2b = invocable_traits::get<type2b>;
    invocable_traits::issue_error<traits2b::error>();
    static_assert(std::is_same_v<void, traits2b::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type2b, int>, traits2b::invoke_result_t>, "");
    static_assert(std::is_same_v<traits2b::invoke_result_t, traits2b::declared_result_t>, "");
    static_assert(std::is_same_v<void, traits2b::class_t>, "");
    static_assert(std::is_same_v<int, traits2b::arg_t<0>>, "");
    static_assert(!traits2b::is_variadic, "");
    static_assert(traits2b::is_noexcept, "");

    using type2c = decltype(&testEllipsis);
    using traits2c = invocable_traits::get<type2c>;
    invocable_traits::issue_error<traits2c::error>();
    static_assert(std::is_same_v<void, traits2c::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type2c, int>, traits2c::invoke_result_t>, "");
    static_assert(std::is_same_v<traits2c::invoke_result_t, traits2c::declared_result_t>, "");
    static_assert(std::is_same_v<int, traits2c::arg_t<0>>, "");
    static_assert(traits2c::is_variadic, "");

    auto& fref = test;
    using type2d = decltype(fref);
    using traits2d = invocable_traits::get<type2d>;
    invocable_traits::issue_error<traits2d::error>();
    static_assert(std::is_same_v<void, traits2d::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type2d, int>, traits2d::invoke_result_t>, "");
    static_assert(std::is_same_v<traits2d::invoke_result_t, traits2d::declared_result_t>, "");
    static_assert(std::is_same_v<int, traits2d::arg_t<0>>, "");


    using type2e = std::reference_wrapper<decltype(test)>;
    using dfdag = std::invoke_result_t<type2e, int>;
    using traits2e = invocable_traits::get<type2e>;
    invocable_traits::issue_error<traits2e::error>();
    static_assert(std::is_same_v<void, traits2e::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type2e, int>, traits2e::invoke_result_t>, "");
    static_assert(std::is_same_v<traits2e::invoke_result_t, traits2e::declared_result_t>, "");
    static_assert(std::is_same_v<int, traits2e::arg_t<0>>, "");

    void (*farr[3])(int) = { &test };
    using type2f = decltype(farr[0]);
    using traits2f = invocable_traits::get<type2f>;
    invocable_traits::issue_error<traits2f::error>();
    static_assert(std::is_same_v<void, traits2f::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type2f, int>, traits2f::invoke_result_t>, "");
    static_assert(std::is_same_v<traits2f::invoke_result_t, traits2f::declared_result_t>, "");
    static_assert(std::is_same_v<int, traits2f::arg_t<0>>, "");

    using type2g = decltype(&testTemplated<float>);
    using traits2g = invocable_traits::get<type2g>;
    invocable_traits::issue_error<traits2g::error>();
    static_assert(std::is_same_v<double, traits2g::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type2g, float, int>, traits2g::invoke_result_t>, "");
    static_assert(std::is_same_v<traits2g::invoke_result_t, traits2g::declared_result_t>, "");
    static_assert(std::is_same_v<void, traits2g::class_t>, "");
    static_assert(std::is_same_v<float, traits2g::arg_t<0>>, "");
    static_assert(std::is_same_v<char, traits2g::arg_t<1>>, "");

    void(&overloadRef)(int, double) = testOver;
    using type2h = decltype(overloadRef);
    using traits2h = invocable_traits::get<type2h>;
    invocable_traits::issue_error<traits2h::error>();
    static_assert(std::is_same_v<void, traits2h::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type2h, int, double>, traits2h::invoke_result_t>, "");
    static_assert(std::is_same_v<traits2h::invoke_result_t, traits2h::declared_result_t>, "");
    static_assert(std::is_same_v<int, traits2h::arg_t<0>>, "");


    using type3 = decltype(&tester::yolo);
    using traits3 = invocable_traits::get<type3>;
    invocable_traits::issue_error<traits3::error>();
    static_assert(std::is_same_v<int, traits3::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type3, tester, char>, traits3::invoke_result_t>, "");
    static_assert(std::is_same_v<traits3::invoke_result_t, traits3::declared_result_t>, "");
    static_assert(std::is_same_v<char, traits3::arg_t<0>>, "");
    static_assert(traits3::is_const, "");

    using type3a = const volatile decltype(&tester::yolo);
    using traits3a = invocable_traits::get<type3a>;
    invocable_traits::issue_error<traits3a::error>();
    static_assert(std::is_same_v<int, traits3a::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type3a, tester, char>, traits3a::invoke_result_t>, "");
    static_assert(std::is_same_v<traits3a::invoke_result_t, traits3a::declared_result_t>, "");
    static_assert(std::is_same_v<char, traits3a::arg_t<0>>, "");
    static_assert(traits3::is_const, "");

    using type3b = decltype(&tester::yoloEllipsis);
    using traits3b = invocable_traits::get<type3b>;
    invocable_traits::issue_error<traits3b::error>();
    static_assert(std::is_same_v<void, traits3b::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type3b, tester, char>, traits3b::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type3b, tester, char, char>, traits3b::invoke_result_t>, "");
    static_assert(std::is_same_v<traits3b::invoke_result_t, traits3b::declared_result_t>, "");
    static_assert(std::is_same_v<tester, traits3b::class_t>, "");
    static_assert(std::is_same_v<char, traits3b::arg_t<0>>, "");
    static_assert(traits3b::is_variadic, "");
    static_assert(traits3b::is_noexcept, "");

    using type3c = const volatile decltype(&tester::yoloEllipsis);
    using traits3c = invocable_traits::get<type3c>;
    invocable_traits::issue_error<traits3c::error>();
    static_assert(std::is_same_v<void, traits3c::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type3c, tester, char>, traits3c::invoke_result_t>, "");
    static_assert(std::is_same_v<traits3c::invoke_result_t, traits3c::declared_result_t>, "");
    static_assert(std::is_same_v<tester, traits3c::class_t>, "");
    static_assert(std::is_same_v<char, traits3c::arg_t<0>>, "");
    static_assert(traits3c::is_variadic, "");
    static_assert(traits3c::is_noexcept, "");

    using type3d = decltype(&tester::yoloStatic);
    using traits3d = invocable_traits::get<type3d>;
    invocable_traits::issue_error<traits3d::error>();
    static_assert(std::is_same_v<long, traits3d::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type3d, short>, traits3d::invoke_result_t>, "");
    static_assert(std::is_same_v<traits3d::invoke_result_t, traits3d::declared_result_t>, "");
    static_assert(std::is_same_v<void, traits3d::class_t>, "");
    static_assert(std::is_same_v<short, traits3d::arg_t<0>>, "");
    static_assert(!traits3d::is_variadic, "");

    int(testerTemplated<char>:: * overloadClassPtr)(char, int) const = &testerTemplated<char>::yolo<int>;
    using type3e = decltype(overloadClassPtr);
    using traits3e = invocable_traits::get<type3e>;
    invocable_traits::issue_error<traits3e::error>();
    static_assert(std::is_same_v<int, traits3e::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type3e, testerTemplated<char>, char, int>, traits3e::invoke_result_t>, "");
    static_assert(std::is_same_v<traits3e::invoke_result_t, traits3e::declared_result_t>, "");
    static_assert(std::is_same_v<testerTemplated<char>, traits3e::class_t>, "");
    static_assert(std::is_same_v<char, traits3e::arg_t<0>>, "");
    static_assert(std::is_same_v<int, traits3e::arg_t<1>>, "");
    static_assert(traits3e::is_const, "");
    static_assert(!traits3e::is_variadic, "");

    short(testerTemplated<char>::* const volatile overloadClassPtr2)(double, float, ...) = &testerTemplated<char>::yolo<float>;
    using type3f = decltype(overloadClassPtr2);
    using traits3f = invocable_traits::get<type3f>;
    invocable_traits::issue_error<traits3f::error>();
    static_assert(std::is_same_v<short, traits3f::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type3f, testerTemplated<char>, double, float>, traits3f::invoke_result_t>, "");
    static_assert(std::is_same_v<traits3f::invoke_result_t, traits3f::declared_result_t>, "");
    static_assert(std::is_same_v<testerTemplated<char>, traits3f::class_t>, "");
    static_assert(std::is_same_v<double, traits3f::arg_t<0>>, "");
    static_assert(std::is_same_v<float, traits3f::arg_t<1>>, "");
    static_assert(!traits3f::is_const, "");
    static_assert(!traits3f::is_volatile, "");
    static_assert(traits3f::is_variadic, "");

    using type3g = decltype(&testerTemplated<void>::yoloEllipsis);
    using traits3g = invocable_traits::get<type3g>;
    invocable_traits::issue_error<traits3g::error>();
    static_assert(std::is_same_v<void, traits3g::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type3g, testerTemplated<void>, char>, traits3g::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type3g, testerTemplated<void>, char, char>, traits3g::invoke_result_t>, "");
    static_assert(std::is_same_v<traits3g::invoke_result_t, traits3g::declared_result_t>, "");
    static_assert(std::is_same_v<testerTemplated<void>, traits3g::class_t>, "");
    static_assert(std::is_same_v<char, traits3g::arg_t<0>>, "");
    static_assert(traits3g::is_variadic, "");
    static_assert(traits3g::is_noexcept, "");

    using type3h = std::reference_wrapper<decltype(&tester::yoloEllipsis)>;
    using traits3h = invocable_traits::get<type3h>;
    invocable_traits::issue_error<traits3h::error>();
    static_assert(std::is_same_v<void, traits3h::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type3h, tester, int>, traits3h::invoke_result_t>, "");
    static_assert(std::is_same_v<traits3h::invoke_result_t, traits3h::declared_result_t>, "");
    static_assert(std::is_same_v<char, traits3h::arg_t<0>>, "");
    static_assert(traits3h::is_variadic, "");
    static_assert(traits3h::is_noexcept, "");
    static_assert(traits3h::arity == 1, "");

    using type3i = decltype(&testerTemplated<char>::yoloStatic<int>);
    using traits3i = invocable_traits::get<type3i>;
    invocable_traits::issue_error<traits3i::error>();
    static_assert(std::is_same_v<long, traits3i::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type3i, short>, traits3i::invoke_result_t>, "");
    static_assert(std::is_same_v<traits3i::invoke_result_t, traits3i::declared_result_t>, "");
    static_assert(std::is_same_v<void, traits3i::class_t>, "");
    static_assert(std::is_same_v<int, traits3i::arg_t<0>>, "");
    static_assert(!traits3i::is_variadic, "");

    using type4 = const volatile decltype(&tester::field);
    using traits4 = invocable_traits::get<type4>;
    invocable_traits::issue_error<traits4::error>();
    static_assert(std::is_same_v<std::invoke_result_t<type4, tester>, traits4::invoke_result_t>, "");
    static_assert(!std::is_same_v<traits4::invoke_result_t, traits4::declared_result_t>, "");
    static_assert(std::is_same_v<const int&&, traits4::invoke_result_t>, "");
    static_assert(traits4::arity == 0, "");

    using type4b = std::reference_wrapper<const volatile decltype(&tester::field)>;
    using traits4b = invocable_traits::get<type4b>;
    invocable_traits::issue_error<traits4b::error>();
    static_assert(std::is_same_v<std::invoke_result_t<type4b, tester>, traits4::invoke_result_t>, "");
    static_assert(!std::is_same_v<traits4b::invoke_result_t, traits4::declared_result_t>, "");
    static_assert(std::is_same_v<const int&&, traits4b::invoke_result_t>, "");
    static_assert(traits4b::arity == 0, "");


    using type5 = std::add_rvalue_reference_t<decltype(lamb)>;
    using traits5 = invocable_traits::get<type5>;
    invocable_traits::issue_error<traits5::error>();
    static_assert(std::is_same_v<const char*, traits5::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type5, int>, traits5::invoke_result_t>, "");
    static_assert(std::is_same_v<traits5::invoke_result_t, traits5::declared_result_t>, "");
    static_assert(std::is_same_v<const int&, traits5::arg_t<0>>, "");

    using type6 = std::add_lvalue_reference_t<decltype(lamb)>;
    using traits6 = invocable_traits::get<type6>;
    invocable_traits::issue_error<traits6::error>();
    static_assert(std::is_same_v<const char*, traits6::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type6, int>, traits6::invoke_result_t>, "");
    static_assert(std::is_same_v<traits6::invoke_result_t, traits6::declared_result_t>, "");
    static_assert(std::is_same_v<decltype(lamb), traits6::class_t>, "");
    static_assert(std::is_same_v<const int&, traits6::arg_t<0>>, "");

    // functors
    using type7 = tester;
    using traits7 = invocable_traits::get<type7>;
    invocable_traits::issue_error<traits7::error>();
    static_assert(std::is_same_v<void, traits7::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type7, int>, traits7::invoke_result_t>, "");
    static_assert(std::is_same_v<traits7::invoke_result_t, traits7::declared_result_t>, "");
    static_assert(std::is_same_v<int, traits7::arg_t<0>>, "");
    static_assert(!traits7::is_const, "");
    static_assert(!traits7::is_noexcept, "");

    using type7b = std::add_lvalue_reference_t<const functorOverloaded>;
    using traits7b = invocable_traits::get<type7b, int>;
    invocable_traits::issue_error<traits7b::error>();
    static_assert(std::is_same_v<int, traits7b::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type7b, const int&>, traits7b::invoke_result_t>, "");
    static_assert(std::is_same_v<traits7b::invoke_result_t, traits7b::declared_result_t>, "");
    static_assert(std::is_same_v<const int&, traits7b::arg_t<0>>, "");
    static_assert(traits7b::is_const, "");
    static_assert(!traits7b::is_noexcept, "");
    static_assert(traits7b::is_variadic, "");
    static_assert(!traits7b::is_exact_match, "");
    static_assert(traits7b::num_matched_overloads == 2, "");
    static_assert(std::is_same_v<const int&, traits7b::matched_overload<0>::arg_t<0>>, "");
    static_assert(std::is_same_v<int&&, traits7b::matched_overload<1>::arg_t<0>>, "");


    using type7c = std::reference_wrapper<const functorOverloaded>;
    using traits7c = invocable_traits::get<type7c, const int&>;
    invocable_traits::issue_error<traits7c::error>();
    static_assert(std::is_same_v<int, traits7c::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type7c, const int&>, traits7c::invoke_result_t>, "");
    static_assert(std::is_same_v<traits7c::invoke_result_t, traits7c::declared_result_t>, "");
    static_assert(std::is_same_v<const int&, traits7c::arg_t<0>>, "");
    static_assert(traits7c::is_const, "");
    static_assert(!traits7c::is_noexcept, "");
    static_assert(traits7c::is_variadic, "");
    static_assert(traits7c::is_exact_match, "");
    static_assert(std::is_same_v<traits7c::arg_t<0>, traits7c::matched_overload<0>::arg_t<0>>, "");

    using type7d = std::add_lvalue_reference_t<const functorOverloaded>;
    using traits7d = invocable_traits::get<type7d, short, int, char>;
    invocable_traits::issue_error<traits7d::error>();
    static_assert(std::is_same_v<int, traits7d::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type7d, const int&>, traits7d::invoke_result_t>, "");
    static_assert(std::is_same_v<traits7d::invoke_result_t, traits7b::declared_result_t>, "");
    static_assert(std::is_same_v<short, traits7d::arg_t<0>>, "");
    static_assert(!traits7d::is_const, "");
    static_assert(!traits7d::is_noexcept, "");
    static_assert(!traits7d::is_variadic, "");
    static_assert(!traits7d::is_exact_match, "");
    static_assert(traits7d::num_matched_overloads == 1, "");
    static_assert(std::is_same_v<const char&, traits7d::matched_overload<0>::arg_t<2>>, "");


    using type7t = functorTemplated;
    using traits7t = invocable_traits::get<type7t, const int&>;
    invocable_traits::issue_error<traits7t::error>();
    static_assert(std::is_same_v<int, traits7t::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type7t, int>, traits7t::invoke_result_t>, "");
    static_assert(std::is_same_v<traits7t::invoke_result_t, traits7t::declared_result_t>, "");
    static_assert(std::is_same_v<type7t, traits7t::class_t>, "");
    static_assert(std::is_same_v<const int&, traits7t::arg_t<0>>, "");
    static_assert(traits7t::is_const, "");


    /*using type7e = functorOverloaded;
    using traits7e = invocable_traits::get<type7e>;
    invocable_traits::issue_error<traits7e::error>();
    static_assert(std::is_same_v<int, traits7e::invoke_result_t>, "");

    using type7f = functorOverloaded;
    using traits7f = invocable_traits::get<type7f, char>;
    invocable_traits::issue_error<traits7f::error>();
    static_assert(std::is_same_v<int, traits7f::invoke_result_t>, "");
    static_assert(std::is_same_v<traits7f::arg_t<0>, traits7f::matched_overload<0>::arg_t<0>>, "");*/


    auto lamb2 = [](const int& in_, ...) mutable noexcept {return "ret"; };

    using type8 = decltype(lamb2);
    using traits8 = invocable_traits::get<type8>;
    invocable_traits::issue_error<traits8::error>();
    static_assert(std::is_same_v<const char*, traits8::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type8, int>, traits8::invoke_result_t>, "");
    static_assert(std::is_same_v<traits8::invoke_result_t, traits8::declared_result_t>, "");
    static_assert(std::is_same_v<const int&, traits8::arg_t<0>>, "");
    static_assert(traits8::is_variadic, "");
    static_assert(traits8::is_noexcept, "");
    static_assert(!traits8::is_const, "");


    auto lamb3 = [](const auto& in_) mutable noexcept {return "ret"; };

    using type8b = decltype(lamb3);
    using traits8b = invocable_traits::get<type8b, const int&>;
    invocable_traits::issue_error<traits8b::error>();
    static_assert(std::is_same_v<const char*, traits8b::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type8b, int>, traits8b::invoke_result_t>, "");
    static_assert(std::is_same_v<traits8b::invoke_result_t, traits8b::declared_result_t>, "");
    static_assert(std::is_same_v<const int&, traits8b::arg_t<0>>, "");
    static_assert(traits8b::is_noexcept, "");
    static_assert(!traits8b::is_const, "");

    using type8c = const std::function<void(const tester&, int)>;
    using traits8c = invocable_traits::get<type8c>;
    invocable_traits::issue_error<traits8c::error>();
    static_assert(std::is_same_v<void, traits8c::invoke_result_t>, "");
    static_assert(std::is_same_v<std::invoke_result_t<type8c, tester, int>, traits8c::invoke_result_t>, "");
    static_assert(std::is_same_v<traits8c::invoke_result_t, traits8c::declared_result_t>, "");
    static_assert(std::is_same_v<const tester&, traits8c::arg_t<0>>, "");
    static_assert(traits8c::is_const, "");


    /*using traits9 = invocable_traits::get<int>;
    invocable_traits::issue_error<traits9::error>();
    static_assert(std::is_same_v<const char*, traits9::invoke_result_t>, "");
    static_assert(std::is_same_v<const int&, traits9::arg_t<10>>, "");*/
}
\$\endgroup\$
4
  • 1
    \$\begingroup\$ godbolt has compliance view to see several compiler at a time Demo. Fix all warnings too. \$\endgroup\$
    – Jarod42
    Jan 13 at 16:26
  • \$\begingroup\$ @Jarod42 Thanks, thats really helpful. I'll edit v5 as i haven't received a review yet. \$\endgroup\$ Jan 13 at 22:09
  • \$\begingroup\$ @Jarod42: updated now, i have fixed all warnings except some originating from the test cases (and some information noise from MSVC) \$\endgroup\$ Jan 13 at 23:19
  • 1
    \$\begingroup\$ fixed warning version (still some noise for msvc). \$\endgroup\$
    – Jarod42
    Jan 14 at 9:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.