C++26 Reflection: Why Reflection Works for Lambdas but Fails for Functions (and How to Fix It)
The Problem: Type Reflection vs. Declaration Reflection
In C++26's experimental reflection library (P2996), there is a fundamental distinction between reflecting on a type and reflecting on a declaration. This distinction is the exact reason why your code works seamlessly for lambdas but fails for standard functions.
Why the Lambda Works
When you pass a lambda to your func_info function, the template parameter Func is deduced as the unique, compiler-generated class type of the lambda. Inside func_info, the condition std::meta::is_class_type(^^F) evaluates to true.
Your code then calls:
func_info_impl<^^F::operator()>();Here, ^^F::operator() refers to the member function declaration of the lambda's call operator. Because this is a declaration reflection, it contains complete metadata about the function, including its parameters and their actual names (identifiers like d and flag).
Why the Regular Function Fails
When you pass func (a regular function) to func_info, it decays into a function pointer. The deduced type Func becomes void (*)(int, const std::string&, float). After removing the pointer, F is the function signature type: void(int, const std::string&, float).
Because this is not a class type, the else branch is executed:
return func_info_impl<^^F>();Here lies the issue: ^^F is a reflection of a type (the function signature), not a reflection of the function declaration. In C++, a function type signature does not carry parameter names—only their types. Consequently, calling std::meta::parameters_of on a type reflection is invalid and throws a compile-time meta-exception because there are no parameter declarations associated with a pure type.
The Solution: Passing the Declaration Reflection Directly
To fix this, you need to pass the reflection of the function declaration itself (using the ^^ reflection operator on the function name) rather than passing the function as a value and relying on type deduction.
Here is how you can modify your code to support both lambdas and regular functions cleanly using C++26 reflection:
#include <meta>
#include <print>
#include <string>
#include <type_traits>
// Helper to extract parameters from a function declaration reflection
template<std::meta::info FuncDecl>
constexpr std::string func_info_impl()
{
static constexpr auto params = std::define_static_array(std::meta::parameters_of(FuncDecl));
std::string result;
template for (constexpr auto p : params)
{
result += "param: ";
result += std::meta::display_string_of(std::meta::type_of(p));
result += " [";
result += std::meta::has_identifier(p) ? std::meta::identifier_of(p) : "<unknown>";
result += "]\n";
}
return result;
}
// Unified reflection entry point
template<std::meta::info Refl>
constexpr std::string func_info()
{
if constexpr (std::meta::is_type(Refl)) {
// If we passed a lambda type, we reflect on its operator()
using T = typename typename(Refl);
return func_info_impl<^^T::operator()>();
} else {
// If we passed a function declaration directly
return func_info_impl<Refl>();
}
}
void func([[maybe_unused]] int a, [[maybe_unused]] const std::string& str, [[maybe_unused]] float f)
{}
int main()
{
// For lambdas, we pass the reflection of its type
auto lambda = []([[maybe_unused]] double d, [[maybe_unused]] bool flag) {};
std::println("lambda:\n{}", func_info<^^decltype(lambda)>());
// For regular functions, we pass the reflection of the declaration directly
std::println("func:\n{}", func_info<^^func>());
}How This Works:
- For Lambdas: We pass
^^decltype(lambda). The template detects it as a type reflection, extracts the type, and reflects on itsoperator()declaration. - For Functions: We pass
^^func. The template receives the function declaration reflection directly, bypassing the type-decay issue entirely, preserving parameter names likea,str, andf.