What the heck is std::meta::info?

What the heck is std::meta::info?

In my last post, we met the reflection operator. But we left the question about std::meta::info hanging. Now it's time to find an answer.

To kick things off, I would like to recall the definition of std::meta::info from P2996R13,Section 4.3.

namespace std {
  namespace meta {
    using info = decltype(^^::);
  }
}

I know, you were expecting something fancier... But don't you think that smaller code blocks are more complicated than they look? Let me elaborate.

The first two lines just define the place where the reflection API lives. The magic happens on line three. info is defined as a type alias instead of a class. Why? Well, it's actually a pretty clever design decision. First of all, it's what we call an opaque type, since it's not bound to any specific type. This design leaves room for future language extensions. Also, it doesn't make any assumptions about how types are represented internally – so different compilers are free to implement it however they want. On top of that, it allows you to collect and store reflections in any container without fear of object slicing.

Lemme prove this;

#include <iostream>
#include <meta>
#include <type_traits>

struct AwesomeStruct{};

int main()
{
  static_assert(std::is_same_v<decltype(^^int), decltype(^^AwesomeStruct)>);
  std::cout << "^^int and ^^AwesomeStruct have the same reflection type!\n";
}

If you want to try it: Compiler Explorer

If you compile this, two things happen: the static_assert passes – as expected, since reflections always share the same type, no matter what they reflect. Then the program prints ^^int and ^^AwesomeStruct have the same reflection type!. That's what std::meta::info does. It hides the internals and presents a uniform interface.

What can we do with this...

Well... I can hear you thinking "I get that it's opaque but what am I supposed to do with it? Why would I need it?" Let me explain:

#include <meta>
#include <print>

#define GET_NAME(x) #x

int main()
{
    int myInt{};
    std::println("{:18}: {}", "With C-Style Macro", GET_NAME(myInt));
    std::println("{:18}: {}", "With Reflection", std::meta::identifier_of(^^myInt));
}

If you wanna try it: Compiler Explorer

To get the name of a variable before reflection, macro stringification is one way to do it. But there is one big issue: no compile time safety. If you pass banana to the GET_NAME macro, it'll return happily without any compile-time check. However, if you pass it to identifier_of, it'll fail which is exactly what we want since safety is important :)

#include <meta>
#include <print>

#define GET_NAME(x) #x

int main()
{
    std::println("{:18}: {}", "With C-Style Macro", GET_NAME(banana));
    //std::println("{:18}: {}", "With Reflection", std::meta::identifier_of(^^banana)); // error: 'banana' has not been declared
}

Prints;

With C-Style Macro: banana

To try it: Compiler Explorer

Let's make something more powerful...

Assume that you want to print non-static data member layout of class with types, reflection gives us this capability without any compiler intrinsics or third-party libraries.

Here it is;

#include <meta>
#include <print>
#include <utility>

struct AwesomeStruct
{
    int mInt;
    double mDouble;
    std::pair<double, double> mPDD;
};

int main()
{
    constexpr auto ctx = std::meta::access_context::current();
    std::println("Nonstatic data members of AwesomeStruct");
    template for(constexpr auto m : 
                    std::define_static_array(
                      std::meta::nonstatic_data_members_of(^^AwesomeStruct, ctx)
                    )
                )
    {
        std::println("  - {:10} is of type {}", 
            std::meta::identifier_of(m), 
            std::meta::display_string_of(std::meta::type_of(m))
        );
    }
}

What happens in code?

Well, first you see std::meta::access_context::current() usage which comes with the reflection API – but I won't explain it here since it's a topic for another time. The real work happens with nonstatic_data_members_of which returns a vector of reflection values of a given type. Since this vector cannot survive into a constant expression, we wrap it in std::define_static_array. Then, for each member, we use std::meta::identifier_of and std::meta::type_of to get each member's name and type, respectively. However, std::meta::type_of returns std::meta::info which std::println cannot print directly so we need to convert it to a string via std::meta::display_string_of. And finally, we get this...

Nonstatic data members of AwesomeStruct
  - mInt       is of type int
  - mDouble    is of type double
  - mPDD       is of type std::pair<double, double>

As always, if you want to try it: Compiler Explorer

And here we have it. We met the operators ^^ and [: :] in the last post. In this post, we dug into std::meta::info – the opaque handle that ties it all together – and saw how metafunctions turn it into something useful: names, types, even a full member layout, all at compile time.

There's a lot more to explore: access control, type generation, and the practical question of compile-time cost – which has already been explored by Vittorio Romeo in cost of enum-to-string: C++26 reflection vs the old ways.

For now, that's a wrap. Next time, we'll go deeper. Thanks for reading! :)


Disclaimer

This post was written by a human, with AI assistance for language polishing.


[P2996R13] Reflection for C++26

Subscribe to Murat Hepeyiler

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe