Reflecting mirrors

Destructure a C++ function

Notes published the
17 - 22 minutes to read, 4314 words
Categories: c++
Keywords: c++ compile-time meta-programming reflection

Sometimes it is useful to be able to extract information from function signatures.

More than once I wanted to static_assert that the first parameter, or the return type, of a function has a specific type.

It turns out, that destructuring a function is not that hard.

#include <tuple>
#include <utility>

template <typename T>
struct function_traits;

template <typename R, typename... Args>
struct function_traits<R(*)(Args...)>
{
    static constexpr std::size_t arity = sizeof...(Args);
    template<std::size_t N>
    using argn = std::tuple_element_t<N, std::tuple<Args...>>;

    using result = R;

    using ptr = R (*)(Args...);
};

using T1 = int(*) ();
void bar(int&);

static_assert(std::is_same_v<function_traits<T1>::result, int>);
static_assert(std::is_same_v<function_traits<decltype(&bar)>::result, void>);

static_assert(function_traits<T1>::arity == 0);
static_assert(function_traits<decltype(&bar)>::arity == 1);
static_assert(std::is_same_v<function_traits<decltype(&bar)>::argn<0>, int&>);


static_assert(std::is_same_v<function_traits<T1>::ptr, T1>);
static_assert(std::is_same_v<function_traits<decltype(&bar)>::ptr, decltype(&bar)>);

The static member variable arity provides the information on how many parameters the function takes, argn<N> gets the N-th parameter type, and result is the type returned by the function or void.

I’m not sure if function_traits::ptr is really useful, especially since the function signature can be reconstructed through the other information exposed in function_traits. But it is easier to recreate here instead of outside of function_traits, thus at the moment I’m going to keep it.

Function values and references

In C++ we also have function values and references, not only pointers.

Adding support for those is trivial, it is possible to extend the current implementation by adding appropriate specialized classes:

#include <tuple>
#include <utility>

template <typename T>
struct function_traits;

template <typename R, typename... Args>
struct function_traits<R(Args...)>
{
    static constexpr std::size_t arity = sizeof...(Args);
    template<std::size_t N>
    using argn = std::tuple_element_t<N, std::tuple<Args...>>;

    using result = R;

    using ptr = R (*)(Args...);
};

template <typename R, typename... Args>
struct function_traits<R(*)(Args...)>: function_traits<R(Args...)>
{};
template <typename R, typename... Args>
struct function_traits<R(&)(Args...)>: function_traits<R(Args...)>
{};

using T1 = int(*) ();
using T2 = void (int);
using T3 = void (&) (int);

static_assert(std::is_same_v<function_traits<T1>::result, int>);
static_assert(std::is_same_v<function_traits<T2>::result, void>);
static_assert(std::is_same_v<function_traits<T3>::result, void>);

static_assert(function_traits<T1>::arity == 0);
static_assert(std::is_same_v<function_traits<T2>::argn<0>, int>);
static_assert(std::is_same_v<function_traits<T3>::argn<0>, int>);

static_assert(std::is_same_v<function_traits<T1>::ptr, T1>);
static_assert(std::is_same_v<function_traits<T2>::ptr, T2*>);
static_assert(std::is_same_v<function_traits<T3>::ptr, T2*>);

noexcept functions

function_traits does still not support an important set of functions: those that are marked noexcept!

Since C++17, noexcept is part of the function type.

For simplicity, I’m going to assume that function_traits will be used with at least C++17, if not, one should remove the code that handles noexcept.

To support those functions, we can, again, extend the current implementation by adding appropriate specialized classes:

#include <tuple>
#include <utility>

template <typename T>
struct function_traits;

template <typename R, typename... Args>
struct function_traits<R(Args...)>
{
    static constexpr std::size_t arity = sizeof...(Args);
    template<std::size_t N>
    using argn = std::tuple_element_t<N, std::tuple<Args...>>;

    using result = R;

    static constexpr bool is_noexcept = false;

    using ptr = R (*)(Args...);
};

template <typename R, typename... Args>
struct function_traits<R(*)(Args...)>: function_traits<R(Args...)>
{};
template <typename R, typename... Args>
struct function_traits<R(&)(Args...)>: function_traits<R(Args...)>
{};

template <typename R, typename... Args>
struct function_traits<R(Args...) noexcept> : function_traits<R(Args...)>
{
    static constexpr bool is_noexcept = true;
    using ptr = R (*)(Args...) noexcept;
};
template <typename R, typename... Args>
struct function_traits<R(*)(Args...) noexcept>: function_traits<R(Args...) noexcept>
{};
template <typename R, typename... Args>
struct function_traits<R(&)(Args...) noexcept>: function_traits<R(Args...) noexcept>
{};

using T1 = int(*) ();
using T2 = void (int);
using T3 = void (&) (int);
using T4 = void (int) noexcept;

static_assert(std::is_same_v<function_traits<T1>::result, int>);
static_assert(std::is_same_v<function_traits<T2>::result, void>);
static_assert(std::is_same_v<function_traits<T3>::result, void>);

static_assert(function_traits<T1>::arity == 0);
static_assert(function_traits<T4>::arity == 1);

static_assert(std::is_same_v<function_traits<T2>::argn<0>, int>);
static_assert(std::is_same_v<function_traits<T3>::argn<0>, int>);

static_assert(std::is_same_v<function_traits<T1>::ptr, T1>);
static_assert(std::is_same_v<function_traits<T2>::ptr, T2*>);
static_assert(std::is_same_v<function_traits<T3>::ptr, T2*>);
static_assert(std::is_same_v<function_traits<T4>::ptr, T4*>);

static_assert(not function_traits<T1>::is_noexcept);
static_assert(    function_traits<T4>::is_noexcept);

member functions

While often overlooked, member functions are functions too, and it would be nice if those were also supported.

Contrary to free functions, member functions might not only be noexcept or not but also const-qualified.

This adds a lot of duplicated code, and probably it is possible to reduce some code duplication with a macro.

In the case of a member function, it is also possible to store the type of the class.

template <typename C, typename R, typename... Args>
struct function_traits<R(C::*)(Args...)>: function_traits<R(Args...)>{
    using owner = C;
};
template <typename C,  typename R, typename... Args>
struct function_traits<R(C::*)(Args...) const>: function_traits<R(Args...)>{
    using owner = const C;
};
template <typename C, typename R, typename... Args>
struct function_traits<R(C::*)(Args...) noexcept>: function_traits<R(Args...) noexcept>{
    using owner = C;
};
template <typename C,  typename R, typename... Args>
struct function_traits<R(C::*)(Args...) const noexcept>: function_traits<R(Args...) noexcept>{
    using owner = const C;
};

struct fwd;
using H1 = int(fwd::*)() noexcept;
using H2 = int(fwd::*)() const;
using H3 = int&(fwd::*)();
using H4 = void(fwd::*)(int);
using H5 = void(fwd::*)(int, bool, char*);

static_assert(function_traits<H1>::is_noexcept);
static_assert(not function_traits<H2>::is_noexcept);

static_assert(std::is_same_v<function_traits<H2>::result, int>);
static_assert(std::is_same_v<function_traits<H3>::result, int&>);
static_assert(std::is_same_v<function_traits<H4>::result, void>);

static_assert(function_traits<H1>::arity == 0);
static_assert(function_traits<H4>::arity == 1);

static_assert(std::is_same_v<function_traits<H4>::argn<0>, int>);
static_assert(std::is_same_v<function_traits<H5>::argn<2>, char*>);


static_assert(std::is_same_v<function_traits<H1>::owner, fwd>);
static_assert(std::is_same_v<function_traits<H2>::owner, const fwd>);


static_assert(std::is_same_v<function_traits<H1>::ptr, int(*)() noexcept>);
static_assert(std::is_same_v<function_traits<H2>::ptr, int(*)()>);

static_assert(std::is_same_v<function_traits<H1>::owner, fwd>);
static_assert(std::is_same_v<function_traits<H2>::owner, const fwd>);

Note that function_traits::ptr is a function pointer and not a member function pointer.

As I’m still not sure if it is useful or not, I’m also not sure what its value should be in the case of member functions.

For distinguishing const from non-const member functions one should query owner.

So far, so good…​ except that member functions can be overloaded by & and && too!

This adds a lot of similar code. At this point, one must wrap it in a macro to avoid copy-paste errors:

template <typename C, typename R, typename... Args>
struct function_traits<R(C::*)(Args...)>: function_traits<R(Args...)>{
    using owner = C;
};
template <typename C, typename R, typename... Args>
struct function_traits<R(C::*)(Args...) noexcept>: function_traits<R(Args...) noexcept>{
    using owner = C;
};

#define MF_TRAIT(MOD)\
    template <typename C, typename R, typename... Args> \
    struct function_traits<R(C::*)(Args...) MOD>: function_traits<R(Args...)>{\
        using owner = C MOD;\
    };\
    template <typename C, typename R, typename... Args> \
    struct function_traits<R(C::*)(Args...) MOD noexcept>: function_traits<R(Args...) noexcept>{\
        using owner = C MOD;\
    }
MF_TRAIT(const  );
MF_TRAIT(     & );
MF_TRAIT(const& );
MF_TRAIT(     &&);
MF_TRAIT(const&&);
#undef MF_TRAIT

struct fwd;
using H1 = int(fwd::*)() noexcept;
using H2 = int(fwd::*)() const;
using H3 = int&(fwd::*)();
using H4 = void(fwd::*)(int);
using H5 = void(fwd::*)(int, bool, char*);

static_assert(function_traits<H1>::is_noexcept);
static_assert(not function_traits<H2>::is_noexcept);

static_assert(std::is_same_v<function_traits<H2>::result, int>);
static_assert(std::is_same_v<function_traits<H3>::result, int&>);
static_assert(std::is_same_v<function_traits<H4>::result, void>);

static_assert(function_traits<H1>::arity == 0);
static_assert(function_traits<H4>::arity == 1);

static_assert(std::is_same_v<function_traits<H4>::argn<0>, int>);
static_assert(std::is_same_v<function_traits<H5>::argn<2>, char*>);


static_assert(std::is_same_v<function_traits<H1>::owner, fwd>);
static_assert(std::is_same_v<function_traits<H2>::owner, const fwd>);


static_assert(std::is_same_v<function_traits<H1>::ptr, int(*)() noexcept>);
static_assert(std::is_same_v<function_traits<H2>::ptr, int(*)()>);

lambdas

While not a function or a member function, it behaves like a function.

It would be nice to be able to destructure a lambda too.

As a lambda is an anonymous structure with an operator(), it is possible to take advantage of the fact that function_traits already works with member functions.

We "just" need to use internally decltype on &T::operator().

#include <tuple>
#include <utility>

template <typename T>
struct function_traits : function_traits<decltype(&T::operator())> {};

template <typename R, typename... Args>
struct function_traits<R(Args...)>
{
    static constexpr std::size_t arity = sizeof...(Args);
    template<std::size_t N>
    using argn = std::tuple_element_t<N, std::tuple<Args...>>;

    using result = R;

    static constexpr bool is_noexcept = false;

    using ptr = R (*)(Args...);
};

template <typename R, typename... Args>
struct function_traits<R(Args...) noexcept> : function_traits<R(Args...)>
{
    static constexpr bool is_noexcept = true;
    using ptr = R (*)(Args...);
};

#define F_TRAIT(PR)\
    template <typename R, typename... Args> \
    struct function_traits<R(PR)(Args...)>: function_traits<R(Args...)>{}; \
    template <typename R, typename... Args> \
    struct function_traits<R(PR)(Args...) noexcept>: function_traits<R(Args...) noexcept>{}
F_TRAIT(*);
F_TRAIT(&);
#undef F_TRAIT

template <typename C, typename R, typename... Args>
struct function_traits<R(C::*)(Args...)>: function_traits<R(Args...)>{
    using owner = C;
};
template <typename C, typename R, typename... Args>
struct function_traits<R(C::*)(Args...) noexcept>: function_traits<R(Args...) noexcept>{
    using owner = C;
};

#define MF_TRAIT(MOD)\
    template <typename C, typename R, typename... Args> \
    struct function_traits<R(C::*)(Args...) MOD>: function_traits<R(Args...)>{\
        using owner = C MOD;\
    };\
    template <typename C, typename R, typename... Args> \
    struct function_traits<R(C::*)(Args...) MOD noexcept>: function_traits<R(Args...) noexcept>{\
        using owner = C MOD;\
    }
MF_TRAIT(const  );
MF_TRAIT(     & );
MF_TRAIT(const& );
MF_TRAIT(     &&);
MF_TRAIT(const&&);
#undef MF_TRAIT

using T1 = int(*) ();
using T2 = void (int);
using T3 = void (&) (int);
using T4 = void (int) noexcept;

static_assert(std::is_same_v<function_traits<T1>::result, int>);
static_assert(std::is_same_v<function_traits<T2>::result, void>);
static_assert(std::is_same_v<function_traits<T3>::result, void>);

static_assert(function_traits<T1>::arity == 0);
static_assert(function_traits<T4>::arity == 1);

static_assert(std::is_same_v<function_traits<T2>::argn<0>, int>);
static_assert(std::is_same_v<function_traits<T3>::argn<0>, int>);

static_assert(std::is_same_v<function_traits<T1>::ptr, T1>);
static_assert(std::is_same_v<function_traits<T2>::ptr, T2*>);
static_assert(std::is_same_v<function_traits<T3>::ptr, T2*>);
static_assert(std::is_same_v<function_traits<T4>::ptr, T4*>);

static_assert(not function_traits<T1>::is_noexcept);
static_assert(    function_traits<T4>::is_noexcept);

struct fwd;
using H1 = int(fwd::*)() noexcept;
using H2 = int(fwd::*)() const;
using H3 = int&(fwd::*)();
using H4 = void(fwd::*)(int);
using H5 = void(fwd::*)(int, bool, char*);

static_assert(function_traits<H1>::is_noexcept);
static_assert(not function_traits<H2>::is_noexcept);

static_assert(std::is_same_v<function_traits<H2>::result, int>);
static_assert(std::is_same_v<function_traits<H3>::result, int&>);
static_assert(std::is_same_v<function_traits<H4>::result, void>);

static_assert(function_traits<H1>::arity == 0);
static_assert(function_traits<H4>::arity == 1);

static_assert(std::is_same_v<function_traits<H4>::argn<0>, int>);
static_assert(std::is_same_v<function_traits<H5>::argn<2>, char*>);


static_assert(std::is_same_v<function_traits<H1>::owner, fwd>);
static_assert(std::is_same_v<function_traits<H2>::owner, const fwd>);


static_assert(std::is_same_v<function_traits<H1>::ptr, int(*)() noexcept>);
static_assert(std::is_same_v<function_traits<H2>::ptr, int(*)()>);

static_assert(function_traits<decltype([]{return 42;})>::arity == 0);

variadic functions

C++ inherited from C variadic functions (not to be confused with a variadic template) too, and the current implementation of function_traits does not support them!

While it is possible to support those too, the arity of a variadic function is a runtime property and not a property known at compile-time. The same holds for the types passed as function parameters.

This means that function_traits should have a flag for distinguishing if the arity is determined at compile-time or not.

volatile functions

Member functions can be overloaded in multiple ways: &, &&, const, …​ and in the mix you should throw volatile too.

At least there are no other qualifiers, and with the macro MF_TRAIT adding support for it is not too much work.

abominable function types

In case someone was asking himself if adding support for variadic functions and const&& overloads is meaningful, rest assured that there are more obscure function types; the Abominable Function Types!

As the paper explains, such functions do not exist, but their types do.

Just like there exists the type function (void(int)), but it is only possible to have function pointer (void(*)(int)) or references (void(&)(int)), the same holds for abominable function types.

It is possible to write a type like void(int, …​) const volatile && noexcept, but it is not possible to create a function, or even a variable declaration, of such type.

At least a const&& member function can be implemented and invoked!

function_traits should support those types for completeness too, at least because std::is_function_v<void(int, …​) const volatile && noexcept> is true, even if probably there are no use-cases for them.

TL;DR

The full implementation of function_traits, with out-of-the-box support for free functions, member functions, lambdas, and abominable function looks like the following:

#include <tuple>
#include <cstddef>

template <typename T>
struct function_traits : function_traits<decltype(&T::operator())> {};

template <typename R, typename... Args>
struct function_traits<R(Args...)>
{
    static constexpr std::size_t arity = sizeof...(Args);
    template<std::size_t N>
    using argn = typename std::tuple_element<N, std::tuple<Args...>>::type;

    using result = R;

#if __cplusplus >= 201703L
    static constexpr bool is_noexcept = false;
#endif
    static constexpr bool is_variadic = false;

    using ptr = R (*)(Args...);
};

#if __cplusplus >= 201703L
template <typename R, typename... Args>
struct function_traits<R(Args...) noexcept> : function_traits<R(Args...)>
{
    static constexpr bool is_noexcept = true;
    using ptr = R (*)(Args...) noexcept;
};
#endif
template <typename R, typename... Args>
struct function_traits<R(Args..., ...)> : function_traits<R(Args...)>
{
    static constexpr bool is_variadic = true;
    using ptr = R (*)(Args..., ...);
};
#if __cplusplus >= 201703L
template <typename R, typename... Args>
struct function_traits<R(Args..., ...) noexcept> : function_traits<R(Args...) noexcept>
{
    static constexpr bool is_noexcept = true;
    static constexpr bool is_variadic = true;
    using ptr = R (*)(Args..., ...) noexcept;
};
#endif



#define F_TRAIT(NOEXCEPT)\
    template <typename R, typename... Args> \
    struct function_traits<R(*)(Args...) NOEXCEPT>: function_traits<R(Args...) NOEXCEPT>{}; \
    template <typename R, typename... Args> \
    struct function_traits<R(*)(Args..., ...) NOEXCEPT>: function_traits<R(Args..., ...) NOEXCEPT>{}; \
    template <typename R, typename... Args> \
    struct function_traits<R(&)(Args...) NOEXCEPT>: function_traits<R(Args...) NOEXCEPT>{}; \
    template <typename R, typename... Args> \
    struct function_traits<R(&)(Args..., ...) NOEXCEPT>: function_traits<R(Args..., ...) NOEXCEPT>{}; \

F_TRAIT(noexcept(false));
#if __cplusplus >= 201703L
F_TRAIT(noexcept(true));
#endif
#undef F_TRAIT

#define F_AB_TRAIT_I(MOD, NOEXCEPT)\
    template <typename R, typename... Args> \
    struct function_traits<R(Args...) MOD NOEXCEPT>: function_traits<R(Args...) NOEXCEPT>{}; \
    template <typename R, typename... Args> \
    struct function_traits<R(Args..., ...) MOD NOEXCEPT>: function_traits<R(Args...) NOEXCEPT>{}

#if __cplusplus >= 201703L
#define F_AB_TRAIT(MOD)\
    F_AB_TRAIT_I(MOD, noexcept(false)); \
    F_AB_TRAIT_I(MOD, noexcept(true))
#else
#define F_AB_TRAIT(MOD) F_AB_TRAIT_I(MOD, noexcept(false));
#endif
F_AB_TRAIT(const volatile   );
F_AB_TRAIT(const            );
F_AB_TRAIT(      volatile   );
F_AB_TRAIT(const volatile & );
F_AB_TRAIT(const          & );
F_AB_TRAIT(      volatile & );
F_AB_TRAIT(               & );
F_AB_TRAIT(const volatile &&);
F_AB_TRAIT(const          &&);
F_AB_TRAIT(      volatile &&);
F_AB_TRAIT(               &&);
#undef F_AB_TRAIT
#undef F_AB_TRAIT_I

template <typename C, typename R, typename... Args>
struct function_traits<R(C::*)(Args...)>: function_traits<R(Args...)>{
    using owner = C;
};

#if __cplusplus >= 201703L
template <typename C, typename R, typename... Args>
struct function_traits<R(C::*)(Args...) noexcept>: function_traits<R(Args...) noexcept>{
    using owner = C;
};
#endif
template <typename C, typename R, typename... Args>
struct function_traits<R(C::*)(Args..., ...)>: function_traits<R(Args..., ...)>{
    using owner = C;
};
#if __cplusplus >= 201703L
template <typename C, typename R, typename... Args>
struct function_traits<R(C::*)(Args..., ...) noexcept>: function_traits<R(Args..., ...) noexcept>{
    using owner = C;
};
#endif

#define MF_TRAIT_I(MOD, NOEXCEPT)\
    template <typename C, typename R, typename... Args> \
    struct function_traits<R(C::*)(Args...) MOD NOEXCEPT>: function_traits<R(Args...) NOEXCEPT>{\
        using owner = C MOD;\
    }; \
    template <typename C, typename R, typename... Args> \
    struct function_traits<R(C::*)(Args..., ...) MOD NOEXCEPT>: function_traits<R(Args..., ...) NOEXCEPT>{\
        using owner = C MOD;\
    }

#if __cplusplus >= 201703L
#define MF_TRAIT(MOD)\
    MF_TRAIT_I(MOD, noexcept(false));\
    MF_TRAIT_I(MOD, noexcept(true))
#else
#define MF_TRAIT(MOD) MF_TRAIT_I(MOD, noexcept(false))
#endif

MF_TRAIT(const             );
MF_TRAIT(               &  );
MF_TRAIT(const          &  );
MF_TRAIT(               && );
MF_TRAIT(const          && );
MF_TRAIT(      volatile    );
MF_TRAIT(const volatile    );
MF_TRAIT(      volatile &  );
MF_TRAIT(const volatile &  );
MF_TRAIT(      volatile && );
MF_TRAIT(const volatile && );
#undef MF_TRAIT


// tests:

using T1 = int(*) (...);
using T2 = void (int);
using T3 = void (&) (int);
#if __cplusplus >= 201703L
using T4 = void (int) noexcept;
#endif

static_assert(std::is_same<function_traits<T1>::result, int>::value, "");
static_assert(std::is_same<function_traits<T2>::result, void>::value, "");
static_assert(std::is_same<function_traits<T3>::result, void>::value, "");

static_assert(function_traits<T1>::arity == 0, "");
static_assert(function_traits<T2>::arity == 1, "");

static_assert(std::is_same<function_traits<T2>::argn<0>, int>::value, "");
static_assert(std::is_same<function_traits<T3>::argn<0>, int>::value, "");

static_assert(std::is_same<function_traits<T1>::ptr, T1>::value, "");
static_assert(std::is_same<function_traits<T2>::ptr, T2*>::value, "");
static_assert(std::is_same<function_traits<T3>::ptr, T2*>::value, "");

#if __cplusplus >= 201703L
static_assert(!function_traits<T1>::is_noexcept);
static_assert( function_traits<T4>::is_noexcept);
#endif

struct fwd;
#if __cplusplus >= 201703L
using H1 = int(fwd::*)() noexcept;
#endif
using H2 = int(fwd::*)() const;
using H3 = int&(fwd::*)();
using H4 = void(fwd::*)(int);
using H5 = void(fwd::*)(int, bool, char*);
using H6 = int(fwd::*)(...) volatile;


#if __cplusplus >= 201703L
static_assert( function_traits<H1>::is_noexcept);
static_assert(!function_traits<H2>::is_noexcept);
#endif

static_assert(std::is_same<function_traits<H2>::result, int>::value, "");
static_assert(std::is_same<function_traits<H3>::result, int&>::value, "");
static_assert(std::is_same<function_traits<H4>::result, void>::value, "");
static_assert(std::is_same<function_traits<H6>::result, int>::value, "");

static_assert(function_traits<H2>::arity == 0, "");
static_assert(function_traits<H4>::arity == 1, "");

static_assert(std::is_same<function_traits<H4>::argn<0>, int>::value, "");
static_assert(std::is_same<function_traits<H5>::argn<2>, char*>::value, "");


static_assert(std::is_same<function_traits<H2>::owner, const fwd>::value, "");
static_assert(std::is_same<function_traits<H3>::owner, fwd>::value, "");


#if __cplusplus >= 201703L
static_assert(std::is_same<function_traits<H1>::ptr, int(*)() noexcept>::value);
#endif
static_assert(std::is_same<function_traits<H2>::ptr, int(*)()>::value, "");

auto l = []{return 42;};
static_assert(function_traits<decltype(l)>::arity == 0, "");

struct s{
    int operator()(bool);
};
static_assert(function_traits<s>::arity == 1, "");

I even went the extra mile and ensured that the code is C++11-compliant (test included), thus making the support for noexcept functions depending on __cplusplus.

Supporting a standard before C++11 is probably not possible, given the lack of variadic macros, and even if, it would require a completely different solution, as std::tuple comes with C++11 too.

Hopefully, I did not forget any free function or member-function signature.


Do you want to share your opinion? Or is there an error, some parts that are not clear enough?

You can contact me anytime.