2
\$\begingroup\$

Update: there are new versions of this code: v2 is posted here, v3 is posted here and v4 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. Questions i have:

  • How to make a nice compiler error message when passing a type that is not callable (such as int at the bottom of the testing code)? These come in at template <typename T> struct callable_traits : callable_traits<decltype(&std::decay_t<T>::operator())>, but i did not manage to test for existence of an operator() or so, so that a static_assert can be issued.
  • See usage inside a function in useInFunc() in the testing code. On MSVC, i need to do traits::template arg<0> instead of traits::arg<0> (typename before isn't necessary there on MSVC, but GCC requires that in turn so thats why that's there). Its a bit unwieldy to have to add the template (because its a dependent type?). Is there a way to avoid that? In general, i wonder why i need to add the typename when used inside the function but not in main for GCC, and why the template for MSVC...
  • I assume this does not work with overloaded functions. Can we catch that, or extend this to make it work?
#pragma once

// derived and extended from https://github.com/kennytm/utils/blob/master/traits.hpp
// and https://stackoverflow.com/a/28213747

#include <tuple>
#include <type_traits>

// get at operator() of any struct/class defining it (this includes lambdas)
template <typename T>
struct callable_traits : callable_traits<decltype(&std::decay_t<T>::operator())>
{};

#define SPEC(cv,unused)                                                         \
/* handle anything with a function-like signature */                            \
template <typename R, typename... Args>                                         \
struct callable_traits<R(Args...) cv>                                           \
{                                                                               \
    using arity = std::integral_constant<std::size_t, sizeof...(Args) >;        \
                                                                                \
    using result_type = R;                                                      \
                                                                                \
    template <std::size_t i>                                                    \
    using arg = typename std::tuple_element<i, std::tuple<Args...,void>>::type; \
};                                                                              \
/* handle pointers to data members */                                           \
template <typename C, typename R>                                               \
struct callable_traits<R C::* cv>                                               \
{                                                                               \
    using arity = std::integral_constant<std::size_t, 0 >;                      \
                                                                                \
    using result_type = R;                                                      \
                                                                                \
    template <std::size_t i>                                                    \
    using arg = typename std::tuple_element<i, std::tuple<void>>::type;         \
};                                                                              \
/* handle pointers to member functions, all possible iterations of reference and noexcept */    \
template <typename C, typename R, typename... Args>                                             \
struct callable_traits<R(C::*)(Args...) cv>             : public callable_traits<R(Args...)> {};\
template <typename C, typename R, typename... Args>                                             \
struct callable_traits<R(C::*)(Args...) cv &>           : public callable_traits<R(Args...)> {};\
template <typename C, typename R, typename... Args>                                             \
struct callable_traits<R(C::*)(Args...) cv &&>          : public callable_traits<R(Args...)> {};\
template <typename C, typename R, typename... Args>                                             \
struct callable_traits<R(C::*)(Args...) cv noexcept>    : public callable_traits<R(Args...)> {};\
template <typename C, typename R, typename... Args>                                             \
struct callable_traits<R(C::*)(Args...) cv & noexcept>  : public callable_traits<R(Args...)> {};\
template <typename C, typename R, typename... Args>                                             \
struct callable_traits<R(C::*)(Args...) cv && noexcept> : public callable_traits<R(Args...)> {};

// cover all const and volatile permutations
SPEC(, )
SPEC(const, )
SPEC(volatile, )
SPEC(const volatile, )

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

testing code:

void test(int)
{

}

template <typename Func>
void useInFunc(Func)
{
    using traits = callable_traits<Func>;
    static_assert(std::is_same_v<const char*, typename traits::result_type>, "");
    static_assert(std::is_same_v<const int&, typename traits::template arg<0>>, "");
}

struct tester
{
    void yolo(char)
    {

    }

    std::string field;
};

int main(int argc, char **argv)
{
    auto lamb = [](const int& in_) noexcept {return "ret"; };

    using traits = callable_traits<decltype(lamb)>;
    static_assert(std::is_same_v<const char*, traits::result_type>, "");
    static_assert(std::is_same_v<const int&, traits::arg<0>>, "");

    useInFunc(lamb);

    using traits2 = callable_traits<decltype(&test)>;
    static_assert(std::is_same_v<void, traits2::result_type>, "");
    static_assert(std::is_same_v<int, traits2::arg<0>>, "");

    using traits3 = callable_traits<decltype(&tester::yolo)>;
    static_assert(std::is_same_v<void, traits3::result_type>, "");
    static_assert(std::is_same_v<char, traits3::arg<0>>, "");

    using traits4 = callable_traits<decltype(&tester::field)>;
    static_assert(std::is_same_v<std::string, traits4::result_type>, "");
    static_assert(std::is_same_v<void, traits4::arg<0>>, "");

    using traits5 = callable_traits<std::add_rvalue_reference_t<decltype(lamb)>>;
    static_assert(std::is_same_v<const char*, traits5::result_type>, "");
    static_assert(std::is_same_v<const int&, traits5::arg<0>>, "");

    using traits6 = callable_traits<std::add_lvalue_reference_t<decltype(lamb)>>;
    static_assert(std::is_same_v<const char*, traits6::result_type>, "");
    static_assert(std::is_same_v<const int&, traits6::arg<0>>, "");

    /*using traits7 = callable_traits<int>;
    static_assert(std::is_same_v<const char*, traits7::result_type>, "");
    static_assert(std::is_same_v<const int&, traits7::arg<0>>, "");*/

    return 0;
}
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Missing support for some types of callables

It fails to compile if you try to get the callable traits of a freestanding function that is noexcept. This is easy to fix by adding:

template <typename R, typename... Args> \
struct callable_traits<R(Args...) cv noexcept>: public callable_traits<R(Args...) cv> {}; \

As mentioned by Jarod42, your code also doesn't handle variadic functions. In the StackOverflow post you referenced in the code, this answer shows one way to add support for that.

Don't append void to the tuple

There should be no need to append void to the tuple you want to get an element's type of. Just write:

template <std::size_t i> \
using arg = typename std::tuple_element<i, std::tuple<Args...>>::type; \

Callers should use arity to check if the function takes any parameters.

Why check for data members?

I don't understand why there is an overload that matches pointers to data members. Those aren't callable, so I would remove that template overload.

Make arity a value

Instead of making arity a type, consider making it a value, like so:

static constexpr auto arity = sizeof...(Args);

This avoids the need to add ::value after it.

Could the macro be avoided?

It would be nice if you didn't need a macro to be able to handle cv-qualifications of functions. There is also some duplication to handle both regular and noexcept functions. Perhaps it can be done by having a four-stage approach:

  1. Get the actual callable. If passed a (pointer to member) function, just return it as-is. If it is a functor, return its operator().
  2. Remove cv-qualifiers and noexcept.
  3. If it is a pointer to member variable, return the function type, otherwise return it as is.
  4. Provide traits of the final non-cv a non-noexcept function type.

This might not be easy. In particular, there is no std::remove_noexcept, so you might have to implement that yourself.

Handling overloaded functions

There is no way you can do that unless you already know the types of arguments and the cv-qualification. Otherwise, the compiler cannot resolve the overload. And if you do know the arguments, you can just use std::invoke_result to get the only missing piece of information (which is the return type).

Why the seemingly unnecessary need for template and typename

Indeed, sometimes the compiler cannot know up front if an identifier refers to a type, variable or function. When parsing the template definition, it has not instantiated the template. So it cannot know for sure what std::tuple_element<i, std::tuple<Args...>>::type is going to be. You remove that ambiguity by adding typename in front of it. Similarly you might need to add template in some places if the compiler would not be able to deduce that, but it seems a shortcoming of the version of MSVC you are using in this case.

\$\endgroup\$
9
  • 1
    \$\begingroup\$ Missing support of C-ellipsis function too (as printf). \$\endgroup\$
    – Jarod42
    Jan 5, 2022 at 8:44
  • \$\begingroup\$ Indeed, C-ellipsis doesn't work, this answer shows how to add support for that. But this reference qualification is working in OP's code as far as I can tell, or did I miss anything? \$\endgroup\$
    – G. Sliepen
    Jan 5, 2022 at 11:04
  • 1
    \$\begingroup\$ Thanks! I have added variadic function support and the missing noexcept free function. Followed the other comments too. Only didn't manage to get rid of the MACROs (which have become more useful now with variadic function support. will post a new version. I check for data members because they are also std::invoke()-able, and have actually a need sometimes to query what type a data member is \$\endgroup\$ Jan 5, 2022 at 11:36
  • \$\begingroup\$ The split between MACRO content and MACRO calls make me miss that this qualification is handled... \$\endgroup\$
    – Jarod42
    Jan 5, 2022 at 11:42
  • \$\begingroup\$ @DiederickC.Niehorster Hm, good point about data members being std::invoke()able. But wouldn't it then be better to set arity to 1 and have arg<0> be C&&? \$\endgroup\$
    – G. Sliepen
    Jan 5, 2022 at 12:42

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.