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 its operator() declaration.
  • For Functions: We pass ^^func. The template receives the function declaration reflection directly, bypassing the type-decay issue entirely, preserving parameter names like a, str, and f.