Skip to content
C++26 - Enter Static Reflection

C++26 - Enter Static Reflection

May 31, 2026

Quite recently it was announced that the release of GCC 16.1 brought in a load of C++26 reflection goodies. From looking at my LinkedIn, it seemed like everyone was extremely excited for this and a load of static reflection articles started popping up on my feed. This all got me a little excited to try it too, and I felt like this problem would be a good way to explore what static reflection has to offer.

Important

Prior to writing the following static reflection code, I had never written a line of static reflection. The largest exposure I had to standard reflection (so not any of the previous draft syntax) was from watching Barry Revzin’s Practical Reflection with C++26 last year. Therefore this should not be taken as a reflection tutorial. I am sure that I am missing out on some cute idioms and stuff like that. This is genuinely my first foray with C++26 reflection.

Step 0: Figuring out how to even use reflection?!

As I say above, I had never used C++26 reflection, and I genuinely didn’t know how to even engage with it. I found CppReference to be quite lacking right now…

I am sure that will change as time goes on, but for right now, it’s a bit rubbish. So I found myself leaning on two other sources of information:

  1. The standard: Specifically Header synopsis. This was pretty damn great for just being able to find my way around all the facilities available. There are links to all the descriptions of what each facility does and what the expected behaviour is.
  2. Barry Revzin’s blog: Barry’s blog has some absolutely brilliant content around reflection, and I can’t recommend it enough.

I will be referencing both of these sources throughout this article.

Step 1: Playing with compilation environments

I have recently got into the habit of wanting to compile my code with at least two compilers. As it turns out, this is a bit of an issue when it comes to reflection currently:

#include <meta>

int Foo(int First, int Second)
{
    return First + Second;
}

int main()
{
    constexpr auto numParams = std::meta::parameters_of(^^Foo).size();
    return numParams;
}

The first thing I wanted to try was just reflecting over a function type, which is what this does. It calls std::meta::parameters_of to get a std::vector<std::meta::info> that holds the parameter reflection infos. We call size on that vector to get the number of parameters and return it. Fairly simple.

However, if you click the link of this snippet, you will find that Clang’s reflection branch doesn’t know what std::meta::parameters_of is yet. This tells us that GCC is probably the only compiler that can compile the code that we want to write!

Knowing this, from here on out, GCC is the only compiler that I will be using. Specifically 16.1. No doubt GCC will continue to add more and more features with newer releases, but we can only work with what we have.

Step 2: The start of HasValidFunctorParamsReflection

First thing I want to do is create a reflection version of HasValidFunctorParams. Obviously we aren’t going to be able to implement it in one go. So let’s just start thinking with reflection:

template<typename FuncType, typename... ArgTypes>
constexpr bool HasValidFunctorParamsReflection = 
    []()
    {
        constexpr std::meta::info funcTypeInfo = ^^FuncType;

        if constexpr (std::meta::is_function_type(funcTypeInfo))
        {
            static_assert(false, "Is Function");
        }
        else if constexpr (std::meta::is_class_type(funcTypeInfo))
        {
            static_assert(false, "Is Class");
        }
        else
        {
            static_assert(false, "No Idea");
        }

        return true;
    }();

namespace HasValidFunctorParamsReflectionTests
{
    void FreeFunc()
    {
    }

    template<typename T>
    void TemplatedFreeFunc(T)
    {

    }

    struct Callable
    {
        void operator()(){}
    };

    using Lambda = decltype([](){});

    auto funcTest = HasValidFunctorParamsReflection<decltype(FreeFunc)>;
    auto funcTest1 = HasValidFunctorParamsReflection<decltype(TemplatedFreeFunc<int>)>;

    auto funcTest2 = HasValidFunctorParamsReflection<Callable>;
    auto funcTest3 = HasValidFunctorParamsReflection<Lambda>;
}

When it comes down to it, there are only two kinds of things that we need to consider:

  1. functions
  2. classes with call operator overloads.

So we start off the implementation of HasValidFunctorParamsReflection with using meta::is_function_type and meta::is_class_type to determine what kind of thing FuncType is. At this point, I have absolutely no idea how I can print anything out. We can’t use cout or print because we are in consteval land here. So I settled on using static_assert to print something out.

If you follow the link you will see that the functions are being reported as functions and the lambda and callable are being reported as classes. Cool!

Step 3: The world of constexpr allocation

One of the first things that I came up against was the following error:

<source>:261:28: error: 'std::meta::parameters_of(((std::meta::info)funcTypeInfo))' is not a constant expression because it refers to a result of 'operator new'
  261 |             constexpr auto parameterTypeInfos = std::meta::parameters_of(funcTypeInfo);
      |    

This was generated from the following code:

template<typename FuncType, typename... ArgTypes>
constexpr bool HasValidFunctorParamsReflection = 
    []()
    {
        constexpr std::meta::info funcTypeInfo = ^^FuncType;

        if constexpr (std::meta::is_function_type(funcTypeInfo))
        {
            constexpr auto parameterTypeInfos = std::meta::parameters_of(funcTypeInfo);
        }
        else if constexpr (std::meta::is_class_type(funcTypeInfo))
        {
            static_assert(false, "Is Class");
        }
        else
        {
            static_assert(false, "No Idea");
        }

        return true;
    }();

Up until I started playing around with reflection I hadn’t really had much experience with constexpr allocation. If you have a look at the <meta> header

you will find that constexpr allocation is everywhere, so this was something I had to get to grips with pretty quickly.

So where is this error coming from? Shouldn’t I be able to assign the result of calling std::meta::parameters_of to a variable in my constexpr function? What would be the point in the standard defining any reflection utility as returning a std::vector in that case?

All of these questions were answered by reading What’s so hard about constexpr allocation?. I am not going to attempt to summarize this fantastic article. I would just encourage you to read it. The upshot of reading that article is that the above code is violating the “Transient allocation rule” which is, “all bytes allocated in a constant expression must be freed by the end of that same constant expression.”

Note

I don’t think this is ever referred to as the “Transient allocation rule”. The concept of transient allocations is mentioned a lot in Barry’s blog and it pretty much explains what I was doing wrong above

Ok, so? We have a std::vector and it will be freed by the end of HasValidFunctorParamsReflection so what is the problem?

The problem is that by marking parameterTypeInfos as constexpr we make that assignment a constant expression. The issue is that once that constant expression completes we have a std::vector. That std::vector’s allocation has just tried to escape the constant expression that created it. This is still the case even if we are in the context of a consteval function.

Ok, so how do we fix it? Pretty simple actually!

template<typename FuncType, typename... ArgTypes>
constexpr bool HasValidFunctorParamsReflection = 
    []()
    {
        constexpr std::meta::info funcTypeInfo = ^^FuncType;

        if constexpr (std::meta::is_function_type(funcTypeInfo))
        {
            const auto parameterTypeInfos = std::meta::parameters_of(funcTypeInfo);
        }
        else if constexpr (std::meta::is_class_type(funcTypeInfo))
        {
            static_assert(false, "Is Class");
        }
        else
        {
            static_assert(false, "No Idea");
        }

        return true;
    }();

Did you see the change? parameterTypeInfos is no longer constexpr. It is just a regular const std::vector. By doing this, the std::vector never escapes the constant expression that did the allocation. HasValidFunctorParamsReflection is the constant expression that performs the allocation, and it is also the one that frees it.

Once I had wrapped my head around this, things started to make a lot more sense!

Step 4: Handling the function type case

Just like with the first article in the series, the first case to tackle is functions. But there is one thing to mention first…

Don’t mix template meta programming and reflection if you don’t have to.

This is something that I caught on to quite quickly. If you stick to pure reflection, your code looks like regular code, which is honestly wonderful. You can throw away all of the template specialization and meta-programming tricks that you have used in the past!

So before we handle the function type case, let’s redefine our IsValid function using reflection:

template<typename FuncType, typename... ArgTypes>
constexpr bool HasValidFunctorParamsReflection = 
    []()
    {
        const auto IsValid =
            [](std::meta::info InReflection)
            {
                return 
                    std::meta::is_reference_type(InReflection) ||
                    (
                        std::meta::is_trivially_copyable_type(InReflection) &&
                        std::meta::size_of(InReflection) <= 16
                    );  
            };

        constexpr std::meta::info funcTypeInfo = ^^FuncType;

        if constexpr (std::meta::is_function_type(funcTypeInfo))
        {
            const auto parameterTypeInfos = std::meta::parameters_of(funcTypeInfo);
        }
        else if constexpr (std::meta::is_class_type(funcTypeInfo))
        {
            static_assert(false, "Is Class");
        }
        else
        {
            static_assert(false, "No Idea");
        }

        return true;
    }();

From what I can see in the spec, many (all?) standard type traits have reflection equivalents. So we can define a regular lambda here which does the same as our previous IsValid but without the templates.

Then handling the function type case is trivial.

template<typename FuncType, typename... ArgTypes>
constexpr bool HasValidFunctorParamsReflection = 
    []()
    {
        const auto IsValid =
            [](std::meta::info InReflection)
            {
                return 
                    std::meta::is_reference_type(InReflection) ||
                    (
                        std::meta::is_trivially_copyable_type(InReflection) &&
                        std::meta::size_of(InReflection) <= 16
                    );  
            };

        constexpr std::meta::info funcTypeInfo = ^^FuncType;

        if constexpr (std::meta::is_function_type(funcTypeInfo))
        {
           for(const auto paramInfo : std::meta::parameters_of(funcTypeInfo))
            {
                if (!IsValid(paramInfo))
                {
                    return false;
                }
            }

            return true;
        }
        else if constexpr (std::meta::is_class_type(funcTypeInfo))
        {
            static_assert(false, "Is Class");
        }
        else
        {
            static_assert(false, "No Idea");
        }

        return true;
    }();

namespace FreeFunc
{
    void SmallByValue(Small, Small){}
    void LargeByValue(Large, Large){}
    void SmallNotTrivialByValue(SmallNotTrivial, SmallNotTrivial){} 

    void SmallByRef(const Small&, const Small&){}
    void LargeByRef(Large&, Large&){}
    void SmallNotTrivialByRef(SmallNotTrivial&, SmallNotTrivial&){} 

    static_assert(HasValidFunctorParamsReflection<decltype(SmallByValue), Small, Small>);
    static_assert(!HasValidFunctorParamsReflection<decltype(LargeByValue), Large, Large>);
    static_assert(!HasValidFunctorParamsReflection<decltype(SmallNotTrivialByValue), SmallNotTrivial, SmallNotTrivial>);

    static_assert(HasValidFunctorParamsReflection<decltype(SmallByRef), Small, Small>);
    static_assert(HasValidFunctorParamsReflection<decltype(LargeByRef), Large, Large>);
    static_assert(HasValidFunctorParamsReflection<decltype(SmallNotTrivialByRef), SmallNotTrivial, SmallNotTrivial>);
}

Remarkably simple, right? We just iterate through our parameters using a regular for loop and call our regular lambda IsValid with the parameter reflection info for each parameter to determine if it is valid or not. It looks like real code! Go back to the end of the first article and compare the difference in these implementations.

Note here also, we have swapped our tests for free functions to use HasValidFunctorParamsReflection. The aim is to gradually remove the need for the original HasValidFunctorParams and just replace it with the reflection version.

Step 5: Refactoring parameter validation

Before we actually start handling the call operators let’s do a small refactor so that we aren’t repeating ourselves in the validation part:

const auto ValidateParameters =
    [IsValid](const std::meta::info InReflection)
    {
        for(const auto paramInfo : std::meta::parameters_of(InReflection))
        {
            if (!IsValid(paramInfo))
            {
                return false;
            }
        }

        return true; 
    };

constexpr std::meta::info funcTypeInfo = ^^FuncType;

if constexpr (std::meta::is_function_type(funcTypeInfo))
{
    return ValidateParameters(funcTypeInfo);
}

Now when we get a std::meta::info of anything that has parameters we can validate it. So this should allow us to handle any and all cases going forward.

Step 6: Handling class types with call operators

This step should be as simple as:

  1. Get the call operator members of our class type
  2. For each call operator, validate the parameters.

Now we can do the fun bit of getting the call operators of a class. And I do mean fun. What I am about to show you is exactly how easy it is to solve the “multiple call operator overload” problem that we painstakingly solved in the last article:

else if constexpr (std::meta::is_class_type(funcTypeInfo))
{
    const std::array argInfos = {^^ArgTypes...};
    for (const auto funcInfo : 
        std::meta::members_of(funcTypeInfo, std::meta::access_context::current())
        | std::views::filter(std::meta::is_operator_function)
        | std::views::filter(
            [](const std::meta::info InOperatorInfo)
            {
                return std::meta::operator_of(InOperatorInfo) == std::meta::operators::op_parentheses;
            }
        )
        | std::views::filter(
            [&](std::meta::info InReflection)
            {
                return std::meta::is_invocable_type(std::meta::type_of(InReflection), argInfos);
            })
        )
    {
        if (!ValidateParameters(std::meta::type_of(funcInfo)))
        {
            return false;
        }
    }

    return true;
}

There is a decent amount of code here, but this is the entire solution that handles classes with any number of call operators. Let’s go through it step by step:

Step 6.1: Reflecting our ArgTypes

The first thing we need to do is get the reflection infos for our argument types:

else if constexpr (std::meta::is_class_type(funcTypeInfo))
{
    const std::array argInfos = {^^ArgTypes...};
}

We will be using these to determine what overloads of the call operator we care about. Note how we are going from having to deal with a variadic pack to just a plain std::array.

Step 6.2: Grabbing the members of a class

We can enumerate the members of any class type using std::meta::members_of:

for (const auto funcInfo : 
        std::meta::members_of(funcTypeInfo, std::meta::access_context::current())
        ...
)

The second argument of type std::meta::access_context tells std::meta::members_of to return the members that are visible from a specified context. In this case, we just want to get the members that are available from the current scope. However, you can also call std::meta::members_of with std::meta::access_context::unchecked() to get all the public and private members.

Step 6.3: Filtering

We don’t want all of a class’ members. We only care about the call operators. Thanks to the fact that we are just operating with a std::vector<std::meta::info>, we can use the ranges library from C++20 to filter our list of members down to the ones that we actually care about.

| std::views::filter(std::meta::is_operator_function)
| std::views::filter(
    [](const std::meta::info InOperatorInfo)
    {
        return std::meta::operator_of(InOperatorInfo) == std::meta::operators::op_parentheses;
    }

These filters will filter out any members which are not call operators. However, we need another filter to make sure that we are only considering the call operators that could be called with our args:

std::views::filter(
    [&](std::meta::info InReflection)
    {
        return std::meta::is_invocable_type(std::meta::type_of(InReflection), argInfos);
    })

std::meta::is_invocable_type is the perfect tool for this job. It takes in an invocable type and a range of std::meta::info which represent the argument types. Notice here how we have to call std::meta::type_of. That is because InReflection here is a member function info. But we need the type of that member function.

Step 6.4: Validating

At this point, we are just iterating over the call operators that we care about, so now we can just go ahead and validate them like any other callable:

for (const auto funcInfo : ...)
{
    if (!ValidateParameters(std::meta::type_of(funcInfo)))
    {
        return false;
    }
}

return true;

And we are done!

At this point we can also switch our tests for overloaded callables over to use HasValidFunctorParamsReflection:

namespace OverloadedCallables
{
    struct OverloadedByVal
    {
        void operator()(Small, Small){}
        void operator()(Large, Large){}
        void operator()(SmallNotTrivial, SmallNotTrivial){}
    };

    struct OverloadedByRef
    {
        void operator()(Small&, Small&){}
        void operator()(Large&, Large&){}
        void operator()(SmallNotTrivial&, SmallNotTrivial&){}
    };

    static_assert(HasValidFunctorParamsReflection<OverloadedByVal, Small, Small>);
    static_assert(!HasValidFunctorParamsReflection<OverloadedByVal, Large, Large>);
    static_assert(!HasValidFunctorParamsReflection<OverloadedByVal, SmallNotTrivial, SmallNotTrivial>);

    static_assert(HasValidFunctorParamsReflection<OverloadedByRef, Small, Small>);
    static_assert(HasValidFunctorParamsReflection<OverloadedByRef, Large, Large>);
    static_assert(HasValidFunctorParamsReflection<OverloadedByRef, SmallNotTrivial, SmallNotTrivial>);
}

And these are all passing. So MUCH easier than it was without reflection!

Step 7: What about Lambdas?..

You might be asking why I have specifically stuck to just the user written classes with call operators up until now. Well, the reason is that due to a GCC 16.1 bug (or just simply a missing part of the implementation) they don’t work :) Let’s see the error we get when we try and switch our lambda tests to using HasValidFunctorParamsReflection

<source>:365:19: error: static assertion failed
  365 |     static_assert(!HasValidFunctorParamsReflection<LargeByValue, Large, Large>);
      |                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:366:19: error: static assertion failed
  366 |     static_assert(!HasValidFunctorParamsReflection<SmallNotTrivialByValue, SmallNotTrivial, SmallNotTrivial>);
      |                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:373:19: error: static assertion failed
  373 |     static_assert(!HasValidFunctorParamsReflection<AutoByVal, Large, Large>);
      |                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:374:19: error: static assertion failed
  374 |     static_assert(!HasValidFunctorParamsReflection<AutoByVal, SmallNotTrivial, SmallNotTrivial>);
      |   

All of the tests where HasValidFunctorParamsReflection is expected to return false are failing. This implies that it is always returning true in the lambda case. Let’s dive into why…

Step 7.1: Debugging with exceptions

Earlier in this article in Step 2 I mentioned that we can print out debug info using static_assert but that really only works when your conditions are constant expressions. Most of the code that we are writing here is within a constant evaluated function, but almost none of that code involves constant expressions. So ideally we need a better way of debugging our constant evaluated reflection code. The folks at JetBrains have added a constexpr debugger to CLion, and I imagine that other IDEs will also start incorporating debugging tools for constexpr code.

BUT! I am doing this all in godbolt, so I don’t have the luxury of this modern invention, so we need something else…

This is where exceptions come in!

Note

Throwing exceptions at constant evaluation time is new to C++26 and a large part of the motivation was to make error handling in reflection code much easier. You can read the paper here: P3068

All error handling (from what I have seen so far) in the reflection library is done via exceptions. To show you an example, let’s take this example from earlier:

std::views::filter(
    [&](std::meta::info InReflection)
    {
        return std::meta::is_invocable_type(std::meta::type_of(InReflection), argInfos);
    })

and let’s remove the std::meta::type_of:

std::views::filter(
    [&](std::meta::info InReflection)
    {
        return std::meta::is_invocable_type(InReflection, argInfos);
    })

When we do this, we will get this compilation error:

<source>: In instantiation of 'constexpr const bool HasValidFunctorParamsReflection<OverloadedCallables::OverloadedByVal, Small, Small>':
<source>:467:19:   required from here
  467 |     static_assert(HasValidFunctorParamsReflection<OverloadedByVal, Small, Small>);
      |                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:255:16: error: uncaught exception of type 'std::meta::exception'; 'what()': 'reflection does not represent a type'
  255 | constexpr bool HasValidFunctorParamsReflection =
      |                ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:467:19: error: non-constant condition for static assertion
  467 |     static_assert(HasValidFunctorParamsReflection<OverloadedByVal, Small, Small>);
      |  

It is throwing here because the std::meta::info that we passed to std::meta::is_invocable_type does not represent a type. It represents a member function. Therefore to get the type of that member function, we need to call std::meta::type_of.

Note

From my exploration around C++26 reflection, these types of errors happen quite a lot because it can be quite easy to lose track of what a certain std::meta::info represents. It is kinda ironic that handling reflection information isn’t strictly type safe in the sense that what it represents is not part of the type of the object. Though because all of this happens at compile time it is functionally type safe… Barry has a great blog around why reflection was designed this way which you can find here

What this means is that we can do printf style debugging by throwing exceptions :)

Step 7.2: Do we even hit our validation code?

We know that this block is always returning true:

else if constexpr (std::meta::is_class_type(funcTypeInfo))
{
    const std::array argInfos = {^^ArgTypes...};
    for (const auto funcInfo : 
        std::meta::members_of(funcTypeInfo, std::meta::access_context::current())
        | std::views::filter(std::meta::is_operator_function)
        | std::views::filter(
            [](const std::meta::info InOperatorInfo)
            {
                return std::meta::operator_of(InOperatorInfo) == std::meta::operators::op_parentheses;
            }
        )
        | std::views::filter(
            [&](std::meta::info InReflection)
            {
                return std::meta::is_invocable_type(std::meta::type_of(InReflection), argInfos);
            })
        )
    {
        if (!ValidateParameters(std::meta::type_of(funcInfo)))
        {
            return false;
        }
    }

    return true;
}

Well… that kinda points to us never reaching inside the for loop body. So let’s verify that by trying to throw an exception in there:

else if constexpr (std::meta::is_class_type(funcTypeInfo))
{
    const std::array argInfos = {^^ArgTypes...};

    std::string message = std::string{"In class Type: "} + std::meta::display_string_of(funcTypeInfo);
    message += "\t Call operator overloads found: ";

    for (const auto funcInfo : 
        ...
        )
    {
        message += std::meta::display_string_of(funcInfo) + std::string{"\n"};
        if (!ValidateParameters(std::meta::type_of(funcInfo)))
        {
            return false;
        }
    }

    throw std::meta::exception(message, ^^HasValidFunctorParamsReflection);

    return true;
}

Here we are just building a string for our exception. There are two things to note here:

  1. Every std::meta::info has a string representation which can be generated by calling std::meta::display_string_of.
  2. std::meta::exception’s second parameter is a reflection token that is used to represent where the exception came from. This can be queried at the point of catching the exception.

From running this code we get the following error message:

<source>:268:16: error: uncaught exception of type 'std::meta::exception'; 'what()': '\x0aIn class Type: Lambda::<lambda(Small, Small)>\x0a Call operator overloads found:\x0a'
  268 | constexpr bool HasValidFunctorParamsReflection =
      |  

Apparently lambda types don’t have any call operator members? What about any members?

else if constexpr (std::meta::is_class_type(funcTypeInfo))
{
    std::string message = std::string{"\nIn class Type: "} + std::meta::display_string_of(funcTypeInfo);
    message += "\n Call operator overloads found:\n";
    for (const auto funcInfo : 
        std::meta::members_of(funcTypeInfo, std::meta::access_context::current()))
    {
        message += std::meta::display_string_of(funcInfo) + std::string{"\n"};
        if (!ValidateParameters(funcInfo))
        {
            return false;
        }
    }

    throw std::meta::exception(message, ^^HasValidFunctorParamsReflection);

    return true;
}

When we remove all the filters from our call to std::meta::members_of we get the same message…

Step 7.3: Lambdas have zero members??

Lambdas are reporting zero members?? Why is that a thing? At this point we need to jump back to the standard to see if this is the expected behaviour. The relevant part is meta.reflection.member.queries:

consteval vector<info> members_of(info r, access_context ctx);
1. A declaration D members-of-precedes a point P if D precedes either P or the point immediately following the class-specifier of the outermost class for which P is in a complete-class context.
2. A declaration D of a member M of a class or namespace Q is Q-members-of-eligible if
    // ...
    (2.6) - if Q is a closure type, then M is a function call operator or function call operator template.

2.6 is quite explicit here. We should be getting members returned. I am quite confident that this is a missing part of GCC’s reflection implementation. It ain’t quite as bad as Clang’s not having std::meta::parameters_of yet, but it is still quite annoying.

Note

Notice how it only mentions call operators here. Lambdas will never return any captured members and that is by design. We all know that lambdas are implemented as class types with a callable operator. However, that is technically just an implementation detail. The committee really want to make sure that they are not dictating implementation with reflection… even if lambdas are classed as “class types”… I personally hope that there will be a way to reflect on captures in C++29 so that I can stop people introducing use-after-frees in lambdas :)

2.6 also mentions “function call operator template”. Might as well see if GCC thinks it has members or not:

template<typename T>
consteval auto GetMemberString()
{
    constexpr auto refl = ^^T; 
    std::string ret;
    ret += "Members of "; 
    ret += std::meta::display_string_of(refl);
    ret += ":\n";

    for (const auto member : std::meta::members_of(refl, std::meta::access_context::current()))
    {
        ret += std::meta::display_string_of(member);
        ret += "\n";
    }

    ret += "---------------------------------\n\n";

    return std::define_static_string(ret);
}

int main()
{
    std::cout << GetMemberString<decltype([](int, float){})>();
    std::cout << GetMemberString<decltype([](auto, auto){})>();
}

When we run this little program, we get the following output:

Members of main()::<lambda(int, float)>:
---------------------------------

Members of main()::<lambda(auto:11, auto:12)>:
template<class auto:11, class auto:12> main()::<lambda(auto:11, auto:12)>
---------------------------------

So non-templated lambdas don’t have members, but templated lambdas do… Ok then!

std::define_static_*

Before moving on, I think it is worth talking through the snippet of code above. To the best of my knowledge, this is the only way to build up a string at compile time. std::format isn’t constexpr yet and I don’t see streams being made constexpr any time soon. As mentioned before, any allocation made in a constant expression must be freed before the end of that constant expression. So the question then becomes, how do we smuggle our std::string out of a constexpr/consteval function without making that allocation last beyond the function? Like most things, reflection has an answer for this in the form of std::define_static_string. This takes any range of characters, and will promote that range to static storage. Since our string is now in static storage, we can refer to it at runtime.

There are two similar facilities provided by the meta library:

  1. std::define_static_array -> useful for promoting a std::vector to a static std::array.
  2. std::define_static_object -> useful for promoting any object to a static constexpr object.

Notice also that these functions are defined in the <meta> header but they are NOT in the std::meta namespace.

Step 8: Handling non-templated lambdas

Until GCC is fixed, we need two things to work around the inability to query the members of non-templated lambdas:

  1. The ability to determine if a type is a non-templated lambda
  2. A way to reflect on a non-templated lambda’s call operator without std::members_of

Step 8.1: Determining if a type is a non-templated lambda

We know a type is a non-templated lambda if:

  1. It has no members
  2. It has a call operator

This makes sure that only non-templated lambdas will be caught by this workaround. Classes with call operators will have members because those operators are members. Here is the detection code:

else if constexpr (std::meta::is_class_type(funcTypeInfo))
{
    const std::array argInfos = {^^ArgTypes...};

    constexpr auto numMembers = std::meta::members_of(funcTypeInfo, std::meta::access_context::current()).size();

    if constexpr (numMembers == 0 && SingleCallOperatorType<FuncType>)
    {
        static_assert(false, "Non templated lambda");
    }
    else
    {
        // Handle other callables like before.
    }
}

I don’t quite like this code for two reasons:

  1. We are forced to call std::meta::members_of twice. We need to call it once in a constant evaluated context to call size on it and store the result in a constexpr local. This is necessary so that we can use the size in an if constexpr branch. And the if constexpr branch is necessary because we will be doing some manipulations of FuncType that are only valid for types with a call operator. The other place where we call std::meta::members_of is in our for loop.
  2. We are having to move out of the land of reflection to invoke a template. In this case the SingleCallOperatorType concept from the previous article. This has made our code more difficult by demanding that we use an if constexpr branch.

Step 8.2: Validating non-template lambdas

This part is relatively easy. We jump back into the world of reflection and validate like before:

else if constexpr (std::meta::is_class_type(funcTypeInfo))
{
    const std::array argInfos = {^^ArgTypes...};

    constexpr auto numMembers = std::meta::members_of(funcTypeInfo, std::meta::access_context::current()).size();

    if constexpr (numMembers == 0 && SingleCallOperatorType<FuncType>)
    {
        return ValidateParameters(std::meta::type_of(^^FuncType::operator()));
    }
    else
    {
        // Handle other callables like before.
    }
}

And with this, we only have 2 failing tests left:

static_assert(!HasValidFunctorParamsReflection<AutoByVal, Large, Large>);
static_assert(!HasValidFunctorParamsReflection<AutoByVal, SmallNotTrivial, SmallNotTrivial>);

Step 9: Handling call operator templates

This is the final hurdle in solving this problem with reflection. The issue comes down to the fact that one of our std::views::filters while filtering our members is this:

std::views::filter(std::meta::is_operator_function)

Call operator templates are not operator functions. They are operator function templates. They are a very different kind of thing. So essentially we are filtering all call operator templates out.

This is what I think should be correct:

else if constexpr (std::meta::is_class_type(funcTypeInfo))
{
    for (const auto funcInfo : 
        std::meta::members_of(funcTypeInfo, std::meta::access_context::current())
        | std::views::transform(
            [&](const std::meta::info InMemberInfo)
            {
                if (std::meta::is_operator_function_template(InMemberInfo) && std::meta::can_substitute(InMemberInfo, argInfos))
                {
                    return std::meta::substitute(InMemberInfo, argInfos);
                }

                return InMemberInfo;
            }
        )
        | std::views::filter(std::meta::is_operator_function)
        | std::views::filter(
            [](const std::meta::info InOperatorInfo)
            {
                return std::meta::operator_of(InOperatorInfo) == std::meta::operators::op_parentheses;
            }
        )
        | std::views::filter(
            [&](std::meta::info InReflection)
            {
                return std::meta::is_invocable_type(std::meta::type_of(InReflection), argInfos);
            })
        )
    {
        if (!ValidateParameters(std::meta::type_of(funcInfo)))
        {
            return false;
        }
    }

    return true;
}

The only difference here is that initial transform. There we are querying for whether it is a function template or not. If it is not, then we just return what was passed in. If it is, then we check if we can substitute our template arguments in to create an instantiated function template. If we can do that, then we do it. From this point on we should have fully instantiated functions that we can filter as before.

There is just one problem… This is still failing, for two reasons:

  1. GCC 16.1 has a bug in it where operator function templates from lambdas never report as being able to be substituted.
  2. GCC 16.1 and trunk (at the time of writing) have bugs in them where instantiated function templates will always result in false being returned from std::meta::is_invocable_type.

The only way I could think of to implement this in GCC 16.1 is with the following:

if constexpr (InvocableTemplate<FuncType, ArgTypes...>)
{
    using InstantiatedFuncType = decltype(&std::remove_reference_t<FuncType>::template operator()<ArgTypes...>);
    using ParamTypes = FunctionTraits<InstantiatedFuncType>::ParamList;
    return AllOf(ParamTypes{},
        []<typename T>()
        {
            return ::IsValid<T>();
        });
}
else
{
    return true;
}

So basically falling back to the old C++20 way of doing things… Which is hugely disappointing and something I would rather not do…

So let’s just take a peek into the future and go with GCC trunk to get us over the line with this one!

const auto members = std::meta::members_of(funcTypeInfo, std::meta::access_context::current());

auto nonTemplatedMembers =
    members
    | std::views::filter(
        [](const std::meta::info InMemberInfo)
        {
            return std::meta::is_operator_function(InMemberInfo);
        }
    )
    | std::views::filter(
        [](const std::meta::info InOperatorInfo)
        {
            return std::meta::operator_of(InOperatorInfo) == std::meta::operators::op_parentheses;
        }
    )
    | std::views::filter(
        [&](std::meta::info InReflection)
        {
            return std::meta::is_invocable_type(std::meta::type_of(InReflection), argInfos);
        }
    );

auto templatedMembers =
    members
    | std::views::filter(
        [&](const std::meta::info InReflection)
        {
            if (std::meta::is_operator_function_template(InReflection))
            {
                const std::meta::info opInstantiation = std::meta::substitute(InReflection, argInfos);

                return 
                    std::meta::is_operator_function(opInstantiation) &&
                    std::meta::operator_of(opInstantiation) == std::meta::operators::op_parentheses; 
            }
            return false;
        });

for (const auto funcInfo : nonTemplatedMembers)
{ 
    if (!ValidateParameters(std::meta::type_of(funcInfo)))
    {
        return false;
    }
}

for (const auto funcInfo : templatedMembers)
{
    const std::meta::info opInstantiation = std::meta::substitute(funcInfo, argInfos);
    if (!ValidateParameters(std::meta::type_of(opInstantiation)))
    {
        return false;
    }
}

return true;

Here we only have one bug to contend with. And the way I have done that is by having two filters. One for non templates, and one for templates. And this works! Not quite as satisfying as just putting in a little transform view before our filters… but it works nonetheless.

And with this we have done it. All of our tests are passing! Here is a link to the final version without any of the old C++20 way of doing it:

Link

Wrapping up

Well, that was a rollercoaster! BUT it was only a rollercoaster because we started hitting the limits of the current most complete implementation. I hope that I have shown that if we had a standard conforming reflection implementation that this problem would be trivial to solve. Not only would it be trivial, but it would also look like regular runtime C++ code. Which is much more than can be said for the C++20 solution to this problem. I personally learned a HUGE amount by making this article, and it has given me a lot of cool ideas on what I can use reflection for. I think the main takeaways from this article are:

  1. Reflection will be super powerful and should make a whole class of previously hard problems, trivial to solve.
  2. Problems are easy to solve if you can stay in the land of std::meta::info
  3. There is a long way to go on documentation for reflection
  4. There are still bugs in even the most complete reflection implementation that currently exists.

I hope you have enjoyed following along, and if you have any thoughts, corrections, or suggestions, don’t be afraid to leave a comment!

Last updated on