Skip to content

# C++26 P2996 Reflection Support

Glaze supports P2996 "Reflection for C++26" as an alternative reflection backend. When enabled, P2996 reflection replaces the traditional `__PRETTY_FUNCTION__` parsing and structured binding tricks with proper compile-time reflection primitives.

## Overview

P2996 was voted into C++26 in June 2025 and provides standardized compile-time reflection capabilities including: - Querying type metadata at compile time - Iterating over struct members - Getting member names and types - Splicing reflected entities back into code

When `GLZ_REFLECTION26` is enabled, Glaze uses P2996 for: - `count_members<T>` - counting struct fields - `to_tie(T&)` - creating a tuple of references to members - `member_nameof<N, T>` - getting the name of the Nth member - `member_names<T>` - array of all member names - `type_name<T>` - getting the type name as a string

The entire Glaze API remains unchanged - JSON, BEVE, CSV, and all other formats work exactly as before.

## Requirements

P2996 reflection requires a compiler with C++26 reflection support:

- GCC 16+: Reflection support merged into GCC trunk
- Available via the Ubuntu Toolchain PPA or by building from source
- See GCC 16 changes for the full list of supported reflection proposals
- Bloomberg clang-p2996: Experimental Clang fork with P2996 support
- Repository: https://github.com/bloomberg/clang-p2996
- Docker image: `vsavkov/clang-p2996:amd64`

### Compiler Flags

GCC 16+:

    g++-16 -std=c++26 -freflection

Bloomberg clang-p2996:

    clang++ -std=c++26 -freflection -fexpansion-statements -stdlib=libc++

Flag | Purpose
--- | ---
`-std=c++26` | Enable C++26 mode
`-freflection` | Enable P2996 reflection
`-fexpansion-statements` | Enable expansion statements (Bloomberg Clang only)
`-stdlib=libc++` | Required for `<meta>` header (Bloomberg Clang only)

## Enabling P2996 Support

### Option 1: CMake (Recommended)

    set(glaze_ENABLE_REFLECTION26 ON)
    FetchContent_MakeAvailable(glaze)

### Option 2: Compiler Define

    clang++ -DGLZ_REFLECTION26=1 -std=c++26 -freflection ...

### Option 3: Automatic Detection

If your compiler defines `__cpp_lib_reflection` or `__cpp_impl_reflection`, Glaze automatically enables P2996 support.

## Feature Detection

Check if P2996 is enabled at compile time:

    #include "glaze/core/feature_test.hpp"

    #if GLZ_REFLECTION26
    // P2996 reflection is available
    #endif

    // Or use the constexpr variable
    if constexpr (glz::has_reflection26) {
        // P2996 code path
    }

## Usage Example

The API is identical whether using P2996 or traditional reflection:

    #include "glaze/glaze.hpp"

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

    int main() {
        // JSON serialization works the same
        Person p{"Alice", 30, 1.65};
        std::string json = glz::write_json(p).value_or("error");
        // {"name":"Alice","age":30,"height":1.65}

        // Member names reflection
        constexpr auto names = glz::member_names<Person>;
        // names == {"name", "age", "height"}

        // Count members
        constexpr auto count = glz::detail::count_members<Person>;
        // count == 3

        // Type name
        constexpr auto type = glz::type_name<Person>;
        // type == "Person"

        // to_tie for member access
        auto tie = glz::to_tie(p);
        glz::get<0>(tie) = "Bob";  // Modifies p.name
    }

## Benefits of P2996

Feature | Traditional | P2996
--- | --- | ---
Max struct members | 128 | Unlimited
Non-aggregate types | Not supported | Full support
Inheritance | Requires explicit `glz::meta` | Automatic (via `nonstatic_data_members_of`)
Member name extraction | `__PRETTY_FUNCTION__` parsing | `std::meta::identifier_of`
Member count | Structured binding probe | `nonstatic_data_members_of().size()`
Private member access | Limited | Full (with `access_context::unchecked()`)
Compile-time safety | Compiler-specific hacks | Standardized API

### Unlimited Member Count

Traditional reflection uses a compile-time binary search with structured bindings, limiting structs to 128 members. P2996 has no such limitation:

    // Works with P2996, would fail with traditional reflection
    struct LargeStruct {
        int field1, field2, /* ... */ field200;
    };

    constexpr auto count = glz::detail::count_members<LargeStruct>;
    // count == 200 (with P2996)

### Non-Aggregate Type Support

Traditional reflection requires types to be aggregates (no user-defined constructors, no private members, no virtual functions, no base classes). P2996 removes this limitation:

    // Classes with custom constructors
    class ConstructedClass {
    public:
        std::string name;
        int value;

        ConstructedClass() : name("default"), value(0) {}
        ConstructedClass(std::string n, int v) : name(std::move(n)), value(v) {}
    };

    // Works with P2996!
    std::string json;
    glz::write_json(ConstructedClass{"test", 42}, json);
    // {"name":"test","value":42}

    // Classes with private members (using glz::meta for access)
    class PrivateMembers {
        std::string secret;
        int hidden;
    public:
        PrivateMembers(std::string s, int h) : secret(std::move(s)), hidden(h) {}
        friend struct glz::meta<PrivateMembers>;
    };

    template <>
    struct glz::meta<PrivateMembers> {
        using T = PrivateMembers;
        static constexpr auto value = object(&T::secret, &T::hidden);
    };

    // Classes with virtual functions
    class VirtualClass {
    public:
        std::string name;
        virtual ~VirtualClass() = default;
        virtual void do_something() {}
    };

    // Works with P2996!
    constexpr auto count = glz::detail::count_members<VirtualClass>;
    // count == 1 (only 'name', virtual function table pointer is not counted)

    // Derived classes - base class members are automatically included!
    class Base {
    public:
        std::string name;
        int id;
        Base() : name("base"), id(0) {}
    };

    class Derived : public Base {
    public:
        std::string extra;
        Derived() : Base(), extra("derived") {}
    };

    // No glz::meta needed! P2996 automatically includes base class members
    std::string json;
    glz::write_json(Derived{}, json);
    // {"name":"base","id":0,"extra":"derived"}

    // Member names include inherited members (base first, then derived)
    constexpr auto names = glz::member_names<Derived>;
    // names == {"name", "id", "extra"}

    constexpr auto count = glz::detail::count_members<Derived>;
    // count == 3 (2 from Base + 1 from Derived)

### Automatic Enum String Serialization

With P2996 enabled, enums can be automatically serialized as strings without writing any `glz::meta` specializations. This uses the `reflect_enums` option.

Since `reflect_enums` is not part of the base `glz::opts`, you enable it by creating a custom opts struct:

    struct reflect_enums_opts : glz::opts {
       bool reflect_enums = true;
    };

    enum class Color { Red, Green, Blue };

    Color c = Color::Green;

    // Write enum as string
    auto json = glz::write<reflect_enums_opts{}>(c).value_or("error");
    // json == "\"Green\""

    // Read string back to enum
    Color c2;
    glz::read<reflect_enums_opts{}>(c2, json);
    // c2 == Color::Green

Enums in structs also work:

    struct Pixel {
       int x;
       int y;
       Color color;
    };

    Pixel p{10, 20, Color::Blue};
    auto json = glz::write<reflect_enums_opts{}>(p).value_or("error");
    // {"x":10,"y":20,"color":"Blue"}

P2996 also provides `enum_to_string` and `string_to_enum` utility functions:

    constexpr auto name = glz::enum_to_string(Color::Red);
    // name == "Red"

    constexpr auto value = glz::string_to_enum<Color>("Blue");
    // value == Color::Blue

    constexpr auto invalid = glz::string_to_enum<Color>("Invalid");
    // invalid == std::nullopt

> Note: Without the `reflect_enums` option, enums without `glz::meta` specializations are still serialized as their underlying integer values, even when P2996 is enabled.

### Cleaner Type Names

P2996 provides cleaner type names via `std::meta::display_string_of`:

    // Traditional: might return "Person" or "struct Person" depending on compiler
    // P2996: returns "Person" consistently
    constexpr auto name = glz::type_name<Person>;

> Breaking Change: P2996 `type_name` returns unqualified names without namespace prefixes, while traditional reflection includes them: - Traditional: `"mylib::MyEnum"` - P2996: `"MyEnum"`

> Code that depends on the exact format of type names (e.g., for key generation in `rename_key`) may produce different output.

### Qualified Type Names Option

> Note: The `qualified_type_names` option is prepared for future P2996 implementations. Bloomberg clang-p2996 does not yet support `std::meta::qualified_name_of`, so this option currently has no effect with P2996. Traditional reflection always returns qualified names regardless of this option.

The `qualified_type_names` option and associated functions (`type_name_for_opts`, `name_for_opts`) are available for forward compatibility:

    struct my_opts : glz::opts {
        bool qualified_type_names = true;
    };

    // These functions are ready for when qualified_name_of becomes available
    constexpr auto name = glz::type_name_for_opts<mylib::MyType, my_opts{}>();
    constexpr auto name2 = glz::name_for_opts<mylib::MyType, my_opts{}>();

When `std::meta::qualified_name_of` is added to the P2996 implementation, these functions will automatically support returning fully-qualified type names with namespace prefixes.

## Docker Development Environment

A Docker container with Bloomberg clang-p2996 can be used for development and testing:

    FROM ubuntu:22.04

    # Install build dependencies
    RUN apt-get update && apt-get install -y \
        git cmake ninja-build python3

    # Clone and build Bloomberg clang-p2996
    RUN git clone https://github.com/bloomberg/clang-p2996.git /opt/llvm-project
    WORKDIR /opt/llvm-project
    RUN cmake -S llvm -B build -G Ninja \
        -DCMAKE_BUILD_TYPE=Release \
        -DLLVM_ENABLE_PROJECTS="clang" \
        -DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi"
    RUN cmake --build build

### Running Tests in Docker

    docker run --rm -v $(pwd):/glaze -w /glaze glaze-p2996-test \
        clang++ -std=c++26 -freflection -fexpansion-statements -stdlib=libc++ \
        -I/glaze/include -DGLZ_REFLECTION26=1 \
        -Wl,-rpath,/opt/llvm-project/build/lib/aarch64-unknown-linux-gnu \
        -o /tmp/test tests/p2996_test/p2996_json_test.cpp && /tmp/test

## Implementation Details

### How P2996 Reflection Works

The P2996 implementation uses these key primitives:

    // Reflect on a type to get meta-info
    constexpr auto type_info = ^^Person;

    // Get all non-static data members
    constexpr auto members = std::meta::nonstatic_data_members_of(
        ^^Person,
        std::meta::access_context::unchecked()
    );

    // Get member name
    constexpr auto name = std::meta::identifier_of(members[0]);
    // name == "name"

    // Splice to access member
    Person p;
    p.[:members[0]:] = "Alice";  // Sets p.name

### Access Context

Glaze uses `access_context::unchecked()` to reflect on all members regardless of access specifiers. This allows P2996 automatic reflection to access private members without requiring friend declarations.

> Note: If you use explicit `glz::meta` specializations with pointer-to-member syntax (e.g., `&T::private_member`), friend declarations are still required because C++ pointer-to-member respects access control. The `access_context::unchecked()` bypass only applies to P2996 automatic reflection.

## Compatibility Notes

- P2996 support is opt-in and does not affect builds using standard compilers
- All existing `glz::meta` specializations continue to work
- The `glz::reflectable<T>` and `glz::has_reflect<T>` concepts work identically
- Custom serializers (`glz::to<JSON>`, `glz::from<JSON>`) are unaffected

## Future

As C++26 compilers mature and P2996 becomes widely available, it will become the preferred reflection mechanism. The traditional `__PRETTY_FUNCTION__` approach will remain for backward compatibility with C++23 compilers.

## See Also

- Reflection in Glaze - General reflection documentation
- Pure Reflection - Automatic struct reflection without metadata
- Modify Reflection - Customizing reflected member names
- Automatic Enum String Serialization - Enum reflection via external libraries