Peeking at C++26 Reflection with the Cat-Ear Operator ^^

Peeking at C++26 Reflection with the Cat-Ear Operator ^^

The ^^ operator added in C++26 (the cat-ears operator) is the gateway to reflection. From a plain struct with no special preparation, you can extract member names and type information at compile time, enabling you to write generic functions like print_struct and to_json in a declarative style.
2026.06.17

This page has been translated by machine translation. View original

Introduction

At the WG21 meeting held in Sofia, Bulgaria in June 2025, one of the proposals adopted for inclusion in C++26 was P2996 (Reflection for C++26). This means that C++ is officially set to gain reflection as a core language feature.

The entry point into P2996 is the star of this article: a new operator called ^^. Because the two carets side by side look like a cat's perky ears, the committee uses the informal name "cat-ears operator." In Japanese, it is often translated as 猫耳演算子 (neko-mimi operator).

This article aims to give engineers who don't normally write C++ a quick sense of what's exciting about this new feature. Rather than diving deep into grammar, we'll explore it through use cases.

Target Audience

  • Engineers who don't normally write C++ but are comfortable with C-style syntax
  • Those who have encountered reflection in other languages (Python, Java, Go, TypeScript, etc.) and are curious about how it feels in C++
  • Those who want to get a general sense of C++26's new features

Verification Environment

  • OS: macOS (darwin 25.5.0, Apple Silicon)
  • Runtime: Docker Desktop 29.4.0 + image vsavkov/clang-p2996:arm64
  • Compiler: Clang 21.0.0git (Bloomberg clang-p2996 fork)
  • Main build options: -std=c++26 -freflection-latest -stdlib=libc++
  • P2996 revision referenced: P2996R13
  • Verification date: 2026-06-17

References

What Is the Cat-Ears Operator ^^?

^^ is a unary prefix operator placed immediately before a name (a type name, function name, variable name, namespace, etc.), written as ^^T. When evaluated, it returns a value of type std::meta::info that represents meta-information about the entity the name refers to. It may be easiest to think of it as the entry point for performing reflection.

The nickname "cat-ears" is exactly what it looks like. Since the Sofia meeting in 2025 where P2996 was adopted into C++26, this name has spread throughout the international community as well.

https://x.com/CPPAlliance/status/2066492940046733732

What's So Great About It?

For those experienced with other languages, the concept of reflection itself may not be unfamiliar. Java's Reflection API lets you enumerate fields and methods from a Class object at runtime. In Python, the dataclasses module and the __dict__ attribute let you peek inside struct-like objects.

It's worth noting that C++ already had techniques for doing reflection-like things. These include X macros, BOOST_FUSION_ADAPT_STRUCT, and boost::hana::struct_. However, all of these approaches required users to write so-called boilerplate. You'd have to line up macro definitions for each struct, or include declarations stating that something is a reflection target.

Compared to these, C++26 reflection differs significantly in that it can extract member names and type information at compile time from a plain, unprepared struct, as-is. Moreover, a splicer syntax [: :] for assembling code from the extracted information is provided as a package deal. This means code whose behavior varies depending on the struct — like a serializer, for example — can now be written declaratively.

Trying It Out: A Generic print_struct

Let's start by writing a print_struct that "prints each member name and value of any struct, one per line."

U1-print-struct.cpp
template <class T>
void print_struct(const T& obj) {
    template for (constexpr auto m :
                  std::define_static_array(
                      std::meta::nonstatic_data_members_of(
                          ^^T, std::meta::access_context::current()))) {
        std::cout << std::meta::identifier_of(m)
                  << ": " << obj.[:m:] << '\n';
    }
}
  • ^^T extracts the type information of the template argument T as a std::meta::info.
  • Passing it to std::meta::nonstatic_data_members_of returns a sequence of infos for the non-static data members of T. The second argument, access_context::current(), is an argument added in P2996R10 and later, meaning "use the current location as the access control context."
  • The return value is a dynamically allocated vector-like type internally, which cannot be passed directly to template for. The current best practice at the time of writing is to bridge it to a static array using std::define_static_array.
  • template for (P1306 expansion statements) is a control construct that expands a compile-time sequence of elements one by one. The loop variable m receives the info for one member at a time.
  • In the body, std::meta::identifier_of(m) retrieves the member's name, and obj.[:m:] retrieves the member's value. [:m:] is the splicer, which plays the role of assembling an "expression" (here, a member access) from an info.

When called, the same call works for different types.

struct Person { std::string name; int age; };
struct Book   { std::string title; int pages; double price; };

Person p{"Alice", 30};
print_struct(p);
std::cout << "---\n";
Book b{"C++ Reflection", 240, 39.95};
print_struct(b);

The output is as follows.

name: Alice
age: 30
---
title: C++ Reflection
pages: 240
price: 39.95

Even though neither Person nor Book appears inside print_struct, the member names and values are printed correctly. This may feel familiar to users of other languages with reflection experience, but what's new in C++26 is that this works with a plain struct — no macros, no boilerplate.

Full sample (U1-print-struct.cpp)
U1-print-struct.cpp
#include <experimental/meta>
#include <iostream>
#include <string>

template <class T>
void print_struct(const T& obj) {
    template for (constexpr auto m :
                  std::define_static_array(
                      std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::current()))) {
        std::cout << std::meta::identifier_of(m) << ": " << obj.[:m:] << '\n';
    }
}

struct Person {
    std::string name;
    int age;
};

struct Book {
    std::string title;
    int pages;
    double price;
};

int main() {
    Person p{"Alice", 30};
    print_struct(p);
    std::cout << "---\n";
    Book b{"C++ Reflection", 240, 39.95};
    print_struct(b);
    return 0;
}

Taking It Further: to_json

Taking the idea from print_struct a step further, you can create a function that "serializes any struct to a JSON-like string" using the same mechanism.

U3-to-json.cpp
template <class T>
std::string to_json(const T& obj) {
    std::ostringstream os;
    os << '{';
    bool first = true;
    template for (constexpr auto m :
                  std::define_static_array(
                      std::meta::nonstatic_data_members_of(
                          ^^T, std::meta::access_context::current()))) {
        if (!first) os << ", ";
        first = false;
        os << '"' << std::meta::identifier_of(m) << "\": ";
        using MT = typename [: std::meta::type_of(m) :];
        if constexpr (std::is_same_v<MT, std::string>) {
            os << '"' << obj.[:m:] << '"';
        } else {
            os << obj.[:m:];
        }
    }
    os << '}';
    return os.str();
}

The key addition compared to print_struct is the line using MT = typename [: std::meta::type_of(m) :];. It extracts type information from the member's info and materializes it as a local type alias MT via a splicer. With that in place, you can then branch on the type using ordinary if constexpr. In the code above, only std::string is wrapped in quotes.

Person p{"Alice", 30};
std::cout << to_json(p) << '\n';
// {"name": "Alice", "age": 30}

To write a serializer before, you either had to write dedicated code for each struct or register it with a macro. With reflection, you need nothing on the struct side — a few dozen lines on the serializer side is enough. Starting from to_json, you can extend the idea to to_yaml, ORM schema generation, automatic test data generation, and more.

Full sample (U3-to-json.cpp)
U3-to-json.cpp
#include <experimental/meta>
#include <iostream>
#include <sstream>
#include <string>
#include <type_traits>

template <class T>
std::string to_json(const T& obj) {
    std::ostringstream os;
    os << '{';
    bool first = true;
    template for (constexpr auto m :
                  std::define_static_array(
                      std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::current()))) {
        if (!first) os << ", ";
        first = false;
        os << '"' << std::meta::identifier_of(m) << "\": ";
        using MT = typename [: std::meta::type_of(m) :];
        if constexpr (std::is_same_v<MT, std::string>) {
            os << '"' << obj.[:m:] << '"';
        } else {
            os << obj.[:m:];
        }
    }
    os << '}';
    return os.str();
}

struct Person {
    std::string name;
    int age;
};

int main() {
    Person p{"Alice", 30};
    std::cout << to_json(p) << '\n';
    return 0;
}

Ideas for Application

Combining ^^ with splicers opens up a variety of other possibilities. Here are four that stood out during verification.

  • Enum to string
    X macros used to be the standard approach, but you can now enumerate enum value infos with std::meta::enumerators_of and compare against actual values via a splicer, all in a template function of a few dozen lines.
  • Generic equality for any struct
    Just replace the output part of print_struct with if (a.[:m:] != b.[:m:]) return false; to get a function that compares all members.
  • Attaching metadata via annotations
    Attach annotations like [[=ColumnIndex{0}]] to members and retrieve them with std::meta::annotations_of. This lets you write ORM column specifications or CLI argument help strings directly in the struct.
  • Enumerating function parameters
    P3096's std::meta::parameters_of lets you extract the name and type of each argument from a function info. This could be useful for simple router generation and similar tasks.
Full sample for enum to string (U2-enum-to-string.cpp)
U2-enum-to-string.cpp
#include <experimental/meta>
#include <iostream>
#include <string_view>

template <class E>
constexpr std::string_view name_of_enum(E v) {
    template for (constexpr auto e :
                  std::define_static_array(std::meta::enumerators_of(^^E))) {
        if (v == [:e:]) return std::meta::identifier_of(e);
    }
    return "<unknown>";
}

enum class Status { Pending = 1, Active = 2, Closed = 3 };

int main() {
    std::cout << name_of_enum(Status::Pending) << '\n';
    std::cout << name_of_enum(Status::Active) << '\n';
    std::cout << name_of_enum(Status::Closed) << '\n';
    std::cout << name_of_enum(static_cast<Status>(99)) << '\n';
    return 0;
}

Summary

In C++26, the combination of the cat-ears operator ^^ and splicers makes it possible to extract and work with member names and type information from a struct that requires no preparation whatsoever on its side. The implementation is still at an experimental stage, but you can open clang-p2996 on Compiler Explorer and get a feel for it right in your browser. I hope this article serves as a useful starting point for exploring C++26 reflection.

Share this article