Function Traits

C++11 has added the incredibly useful type_traits library. However, beyond std::is_function there isn’t really much going on for functions. Rolling your own function traits is not very difficult, and this post will show you how!

A good place to start is free functions. Using templates, it’s easy to unpack the return type, arity and argument types for a function:

template<class F>
struct function_traits;

// function pointer
template<class R, class... Args>
struct function_traits<R(*)(Args...)> : public function_traits<R(Args...)>
{};

template<class R, class... Args>
struct function_traits<R(Args...)>
{
    using return_type = R;

    static constexpr std::size_t arity = sizeof...(Args);

    template <std::size_t N>
    struct argument
    {
        static_assert(N < arity, "error: invalid parameter index.");
        using type = typename std::tuple_element<N,std::tuple<Args...>>::type;
    };
};

Which can be used like this:

float free_function(const std::string& a, int b)
{
    return (float)a.size() / b;
}

int main()
{
    using Traits = function_traits<decltype(free_function)>;

    static_assert(Traits::arity == 2,"");
    static_assert(std::is_same<Traits::return_type,float>::value,"");
    static_assert(std::is_same<Traits::argument<0>::type,const std::string&>::value,"");
    static_assert(std::is_same<Traits::argument<1>::type,int>::value,"");

    return 0;
}

The use of decltype is necessary because we need the type of the function, not the function itself. Here, decltype(free_function) resolves to the function pointer, but it is entirely possible to do something like function_traits<int(char)>.

Through template pattern matching, the compiler is capable of determining R and Args.... We can then alias return_type and get the arity from the parameter pack. Getting the arguments is slightly more complex, unpacking Args... into a tuple and accessing elements using std::tuple_element.

Functionality can be extended to function-like objects such as member function pointers and member object pointers:

// member function pointer
template<class C, class R, class... Args>
struct function_traits<R(C::*)(Args...)> : public function_traits<R(C&,Args...)>
{};

// const member function pointer
template<class C, class R, class... Args>
struct function_traits<R(C::*)(Args...) const> : public function_traits<R(C&,Args...)>
{};

// member object pointer
template<class C, class R>
struct function_traits<R(C::*)> : public function_traits<R(C&)>
{};

Const and non-const member functions are treated as separate types. This basically treats member function/object pointers as functions which take a reference to the appropriate class.

To handle functors and std::function objects (technically also a functor), we can now implement the default specialization:

// functor
template<class F>
struct function_traits
{
    private:
        using call_type = function_traits<decltype(&F::type::operator())>;
    public:
        using return_type = typename call_type::return_type;

        static constexpr std::size_t arity = call_type::arity - 1;

        template <std::size_t N>
        struct argument
        {
            static_assert(N < arity, "error: invalid parameter index.");
            using type = typename call_type::template argument<N+1>::type;
        };
};

template<class F>
struct traits<F&> : public traits<F>
{};

template<class F>
struct traits<F&&> : public traits<F>
{};

This will determine the type traits of the member operator() function then use that to determine the function traits of the functor itself. The two extra specializations will also strip any reference qualifiers to prevent wierd errors.

Like type traits, these function traits are very useful to debug templates. What was once an indecipherable wall of compiler errors can now be easily caught by a single static assertion. As we move on to more complex functional concepts, these will become invaluable.

I leave the implementation of is_callable, which determines if an object is a function or function-like (function pointer, member function/object pointer, functor), to the reader.

2 comments

  1. Slava

    I guess “What was once an indecipherable wall of debug errors can now be easily caught by a single static assertion” should read “What was once an indecipherable wall of compiler errors can now be easily caught by a single static assertion”

Leave a comment