1
\$\begingroup\$

Update: there is a new version of this code: v5 is posted here


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. This is v4 (v3 here), incorporating comments received on v2 and further enhancements:

  1. the return type when using the invocable with std::invoke() may differ from the declared return type. Can now query both.
  2. can now get more info about the function declaration, whether it is marked const, volatile and no_except

Did i cover all possible callables out there? Do i provide all potentially useful information about a callable (it covers my use cases, but if you can think of more that might be useful, let me know).

#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
// This does not handle overloaded functions (which includes functors with
// overloaded operator()), because the compiler would not be able to resolve
// the overload without knowing the argument types and the cv- and noexcept-
// qualifications. If you do know those already and can thus specify the
// overload to the compiler, you do not need this class. The only remaining
// piece of information is the result type, which you can get with
// std::invoke_result.

namespace detail
{
    template <std::size_t i, typename... Args>
    struct invocable_traits_arg_impl
    {
        static_assert(i < sizeof...(Args), "Requested argument type out of bounds (function does not declare this many arguments)");

        using type = std::tuple_element_t<i, std::tuple<Args...>>;
    };

    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, Args...>::type;
    };

    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...> {};


    template <typename T>
    struct invocable_traits_impl;

    #define IS_NONEMPTY(...) 0 __VA_OPT__(+1)

    #define INVOCABLE_TRAITS_SPEC(c,v,...)                                              \
    /* functions, including noexcept versions */                                        \
    template <typename R, typename... Args>                                             \
    struct invocable_traits_impl<R(Args... __VA_OPT__(,) __VA_ARGS__) c v>              \
        : public invocable_traits_free<                                                 \
            R,                                                                          \
            std::invoke_result_t<R(Args... __VA_OPT__(,) __VA_ARGS__) c v,Args...>,     \
            IS_NONEMPTY(c),                                                             \
            IS_NONEMPTY(v),                                                             \
            false,                                                                      \
            IS_NONEMPTY(__VA_ARGS__),                                                   \
            Args...> {};                                                                \
    template <typename R, typename... Args>                                             \
    struct invocable_traits_impl<R(Args...__VA_OPT__(,) __VA_ARGS__) c v noexcept>      \
        : public invocable_traits_free<                                                 \
            R,                                                                          \
            std::invoke_result_t<R(Args... __VA_OPT__(,) __VA_ARGS__) c v noexcept,Args...>,\
            IS_NONEMPTY(c),                                                             \
            IS_NONEMPTY(v),                                                             \
            true,                                                                       \
            IS_NONEMPTY(__VA_ARGS__),                                                   \
            Args...> {};                                                                \
    /* pointers to member functions, including noexcept versions) */                    \
    template <typename C, typename R, typename... Args>                                 \
    struct invocable_traits_impl<R(C::*)(Args...__VA_OPT__(,) __VA_ARGS__) c v>         \
        : public invocable_traits_class<                                                \
            R,                                                                          \
            std::invoke_result_t<R(C::*)(Args...__VA_OPT__(,) __VA_ARGS__) c v,C,Args...>,  \
            C,                                                                          \
            IS_NONEMPTY(c),                                                             \
            IS_NONEMPTY(v),                                                             \
            false,                                                                      \
            IS_NONEMPTY(__VA_ARGS__),                                                   \
            Args...> {};                                                                \
    template <typename C, typename R, typename... Args>                                 \
    struct invocable_traits_impl<R(C::*)(Args...__VA_OPT__(,) __VA_ARGS__) c v noexcept>\
        : public invocable_traits_class<                                                \
            R,                                                                          \
            std::invoke_result_t<R(C::*)(Args...__VA_OPT__(,) __VA_ARGS__) c v noexcept,C,Args...>,  \
            C,                                                                          \
            IS_NONEMPTY(c),                                                             \
            IS_NONEMPTY(v),                                                             \
            true,                                                                       \
            IS_NONEMPTY(__VA_ARGS__),                                                   \
            Args...> {};

    // cover all const and volatile permutations
    INVOCABLE_TRAITS_SPEC(,, )
    INVOCABLE_TRAITS_SPEC(const,, )
    INVOCABLE_TRAITS_SPEC(,volatile, )
    INVOCABLE_TRAITS_SPEC(const, volatile, )
    // and also variadic function versions
    INVOCABLE_TRAITS_SPEC(,, ...)
    INVOCABLE_TRAITS_SPEC(const,, ...)
    INVOCABLE_TRAITS_SPEC(,volatile, ...)
    INVOCABLE_TRAITS_SPEC(const, volatile, ...)
    #undef INVOCABLE_TRAITS_SPEC

    /* 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> {};


    // get at operator() of any struct/class defining it (this includes lambdas)
    // bit of machinery for better error messages
    template <typename T>
    concept HasCallOperator = requires(T t)
    {
        t.operator();
    };

    template <typename T, bool isCallable>
    struct invocable_traits_extract : invocable_traits_impl<decltype(&T::operator())> {};

    template <typename T>
    struct invocable_traits_extract<T, false>
    {
        static_assert(std::is_class_v<T>, "passed type is not a class, and thus cannot have an operator()");
        static_assert(!std::is_class_v<T> || HasCallOperator<T>, "passed type is a class that doesn't have an operator()");

        // to reduce excessive compiler error output
        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> struct arg_t { using type = void; };
    };

    template <typename T>
    struct invocable_traits_impl : invocable_traits_extract<T, HasCallOperator<T>> {};
}

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

testing code:

void test(int)
{}
void test2(int) noexcept
{}
void testEllipsis(int,...)
{}

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

    static long yoloStatic(short)
    {}

    void operator()(int in_) {}

    const int field;
};


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

    using type1 = decltype(lamb);
    using traits1 = invocable_traits<type1>;
    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 type2 = decltype(&test);
    using traits2 = invocable_traits<type2>;
    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<type2b>;
    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<type2c>;
    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<type2d>;
    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>>, "");

    void (*farr[3])(int) = { &test };
    using type2e = decltype(farr[0]);
    using traits2e = invocable_traits<type2e>;
    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>>, "");


    using type3 = decltype(&tester::yolo);
    using traits3 = invocable_traits<type3>;
    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<type3a>;
    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<type3b>;
    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<type3c>;
    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<type3d>;
    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, "");

    using type4 = const volatile decltype(&tester::field);
    using traits4 = invocable_traits<type4>;
    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 type5 = std::add_rvalue_reference_t<decltype(lamb)>;
    using traits5 = invocable_traits<type5>;
    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<type6>;
    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>>, "");

    // functor
    using type7 = tester;
    using traits7 = invocable_traits<type7>;
    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>>, "");


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

    using type8 = decltype(lamb2);
    using traits8 = invocable_traits<type8>;
    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, "");


    /*using traits9 = invocable_traits<int>;
    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\$
2
  • 1
    \$\begingroup\$ You haven't had any reviews of version 3 yet - either wait a bit longer, or edit your unanswered question instead of creating a new one (N.B. don't edit code of a question that has answers - that's different!) \$\endgroup\$ Jan 7, 2022 at 14:19
  • \$\begingroup\$ ok, will do that next time. I have reviews here now :) \$\endgroup\$ Jan 7, 2022 at 20:17

1 Answer 1

2
\$\begingroup\$

Improving error messages

A question from the earliest version of the code you posted was how to get nicer error messages. The way to get those is to have things fail as quickly as possible, so the error message you get comes from close to the actual use of the invocable_traits class, instead of from somewhere deep in the bowels of its implementation.

Prefer using requires clauses over static_assert() if possible. For example, you can write:

template <std::size_t i, typename... Args>
requires (i < sizeof...(Args))
struct invocable_traits_arg_impl
{
    using type = std::tuple_element_t<i, std::tuple<Args...>>;
};

The compiler will be able to produce much better error messages for constraints, including showing you what the values were on both sides of the < for example.

Second, don't defer constraints to inherited classes. Ideally, you should write something like:

template <typename T>
requires Callable<T>
struct invocable_traits : detail::invocable_traits_impl<std::decay_t<T>> {};

Where the concept Callable would check if T is in fact a callable. This concept would look like:

template <typename T>
concept Callable = (
    PointerToMemberFunction<T>
    || PointerToDataMember<T>
    || FunctionObject<T>
);

Which in turn relies on more concepts that need to be implemented.

#undef any macros you #define

Preprocessor macros know nothing about namespaces, class and function scopes. If you #define any in a header file that should not be part of the public API, make sure you #undef them in the same file. You do this for INVOCABLE_TRAITS_SPEC, but not for IS_NONEMPTY.

\$\endgroup\$
6
  • \$\begingroup\$ Hmm, after testing it, I disagree about using requires clauses over static_assert(). The requires clause allows me to specify a specific error message, which is better than messages produced by a failed requires clause (on MSVC). Having the values would be nice though, hope some form of compile-time formatting makes it into the standard soon. \$\endgroup\$ Jan 8, 2022 at 10:53
  • \$\begingroup\$ I agree it would be great to put the error checks as early as possible. Its now two specializations away (to struct invocable_traits_extract<T, false>). I'm not sure writing all those concepts is worth reducing that further though, as the first thing that hits a user in the face upon error is the static assert message. \$\endgroup\$ Jan 8, 2022 at 10:56
  • \$\begingroup\$ I have made a v5 that allows to select a specific overload in a function object (incl. lambdas), the only case where support for overloads is needed (you can only make function pointers to a specific overload anyway, so no problem there). Its not ideal yet as you need to get the signature exactly right (so need to specify const int&, not just int). Is it ok to do another round of review again? Really appreciate your time and expertise! \$\endgroup\$ Jan 8, 2022 at 10:59
  • \$\begingroup\$ Sure, there is no limit here as far as I know. Just post it :) \$\endgroup\$
    – G. Sliepen
    Jan 8, 2022 at 12:36
  • \$\begingroup\$ static_assert() is indeed nice because you can add your own error message, but the problem is that that by the time you reach the assert, the error message might have little bearing on what was done at the original call site. requires() clauses allow the compiler to connect the problem at the template instantiation to how it is invoked at the call site. For arg<> it's not an issue though, but think I would personally still favor requires; I think the error message that GCC and Clang provide there are not much worse than your assert text. \$\endgroup\$
    – G. Sliepen
    Jan 8, 2022 at 18:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.