Introspecting Functions at Compile Time in C++
I wrote this simple FunctionTraits structure that allows you to inspect function types at compile time, that is, to look “inside” a function and discover what arguments it takes and what it returns.
Here’s the full implementation:
#include <type_traits>
#include <tuple>
template<typename, typename = std::void_t<>>
struct FunctionTraits {};
template<typename Return, typename ...Args>
struct FunctionTraits<Return (*)(Args...)> {
using ReturnType = Return;
using ArgTypes = std::tuple<Args...>;
};
template<typename Return, typename Class, typename ...Args>
struct FunctionTraits<Return (Class::*)(Args...)> {
using ReturnType = Return;
using ArgTypes = std::tuple<Args...>;
};
template<typename Return, typename Class, typename ...Args>
struct FunctionTraits<Return (Class::*)(Args...) const> {
using ReturnType = Return;
using ArgTypes = std::tuple<Args...>;
};
template<typename F>
struct FunctionTraits<F, std::void_t<decltype(&F::operator())>> {
using ReturnType = typename FunctionTraits<decltype(&F::operator())>::ReturnType;
using ArgTypes = typename FunctionTraits<decltype(&F::operator())>::ArgTypes;
};
How It Works#
Let’s break it down piece by piece.
1. The Default Case#
template<typename, typename = std::void_t<>>
struct FunctionTraits {};
This is the primary template. It matches anything by default but doesn’t define anything inside.
It also sets up a SFINAE-friendly structure (std::void_t) so that the compiler can gracefully skip it when other specializations fit better.
2. Free Functions and Function Pointers#
template<typename Return, typename ...Args>
struct FunctionTraits<Return (*)(Args...)> {
using ReturnType = Return;
using ArgTypes = std::tuple<Args...>;
};
This specialization matches plain function pointers, such as:
int foo(double, char);
using Traits = FunctionTraits<decltype(&foo)>;
Now you can do:
using Return = Traits::ReturnType; // int
using Args = Traits::ArgTypes; // std::tuple<double, char>
3. Member Functions#
Member functions are trickier because they’re tied to a class:
template<typename Return, typename Class, typename ...Args>
struct FunctionTraits<Return (Class::*)(Args...)> {
using ReturnType = Return;
using ArgTypes = std::tuple<Args...>;
};
This handles non-const member functions such as:
struct MyClass {
void do_something(int, float);
};
using Traits = FunctionTraits<decltype(&MyClass::do_something)>;
// ReturnType = void, ArgTypes = std::tuple<int, float>
4. Const Member Functions#
Some member functions are const (they don’t modify the object).
We need a separate specialization for that:
template<typename Return, typename Class, typename ...Args>
struct FunctionTraits<Return (Class::*)(Args...) const> {
using ReturnType = Return;
using ArgTypes = std::tuple<Args...>;
};
5. Lambdas and Functors#
Here’s where it gets interesting:
template<typename F>
struct FunctionTraits<F, std::void_t<decltype(&F::operator())>> {
using ReturnType = typename FunctionTraits<decltype(&F::operator())>::ReturnType;
using ArgTypes = typename FunctionTraits<decltype(&F::operator())>::ArgTypes;
};
Every lambda or functor (a class with an overloaded operator()) has a callable operator that looks like a member function.
This specialization detects it using decltype(&F::operator()) and then recursively reuses the member function traits.
So this works:
auto lambda = [](int x, double y) -> bool { return x < y; };
using Traits = FunctionTraits<decltype(lambda)>;
// Traits::ReturnType = bool
// Traits::ArgTypes = std::tuple<int, double>
Putting It All Together#
Let’s see FunctionTraits in action:
#include <iostream>
template <typename F>
void print_function_info(F&&) {
using Traits = FunctionTraits<std::decay_t<F>>;
std::cout << "Return type: " << typeid(typename Traits::ReturnType).name() << "\n";
std::cout << "Number of args: " << std::tuple_size<typename Traits::ArgTypes>::value << "\n";
}
int test_func(int, double) { return 42; }
int main() {
print_function_info(&test_func);
print_function_info([](std::string, float) -> void {});
}
Possible output (compiler-dependent):
Return type: i
Number of args: 2
Return type: v
Number of args: 2
Final Thoughts#
FunctionTraits is a simple/compact yet powerful tool for used function introspection.
With just a few template tricks, std::void_t, variadic templates, and recursive specialization, we have this structure that works on nearly any callable object.