cpp26-reflection · part 22

reflect_dx: auto-generated debugger pretty-printers and docs

· upd 2026-10-06· english· audience: working-cpp· AI-generated, reviewed by Filip Sajdak

Correction, 6 October 2026. This post presents reflect_dx as a published library with two command-line tools, and it is not one. The repository linked below holds a README and one “Initial commit” from 22 June 2026: no code, release, tag or licence. reflect-pretty, reflect-docs, libreflect_dx_core.a and the CMake function reflect_dx_generate() do not exist, so the terminal output and the generated files shown below came from no tool. The Compiler Explorer demo covers one thing: the natvis walk from the sketch near the end. It prints a <Type> element with a DisplayString and an Expand block for two structs, and nothing else (no CLI, no LLDB or GDB formatter, no documentation generator). I ran it on Compiler Explorer (clang-p2996) and on GCC 16.2.0, and both print the same text. Three claims do not work as written. First, reflection cannot read comments: the C++26 reflection API has no comment query (the <meta> synopsis in eel.is/c++draft/meta.syn lists annotations_of and source_location_of, which gives a file and a line, and nothing for comments), so a documentation generator can use identifiers and annotations only. Second, an external binary cannot run std::meta functions over a header it parses: reflection runs inside the compiler while it compiles the program that uses it, and std::meta::namespace_members_of is not a function (GCC 16.2.0: 'namespace_members_of' is not a member of 'std::meta'). What does work is a small generator program that includes the header and walks std::meta::members_of(^^ns, ctx). On GCC 16.2.0 that program listed the fields of two structs in a namespace. It sees the namespace as that one translation unit declares it. Third, natvis_snippet returns std::string from a consteval function: on GCC 16.2.0 a call whose result is used at run time is refused (is not a constant expression), and a constexpr std::string variable holding a longer string is refused too (refers to a result of 'operator new'). The embedded demo returns char const* through std::define_static_string, and that form compiles and runs. I also removed the “share 80%” figure, which was never measured, and the promise of a companion short that was never published. The text below is otherwise left as published, so you can read it against this note.

No published library. github.com/wrocpp/reflect_dx is a placeholder repository with a README only. This post describes a design sketch.

Two universal C++ papercuts this post closes.

Papercut #1: debugger pretty-printers

The default LLDB / GDB / Visual Studio debugger view of a nested struct is a tree of braces:

(lldb) p user
(User) {
  name = {
    _M_local_buf = ...
    _M_string_length = 5
  }
  age = 40
  admin = true
  home = {
    city = { _M_local_buf = ..., _M_string_length = 6 }
    postal_code = 12345
  }
}

You can make this nicer by writing a pretty-printer: a .natvis XML blob for Visual Studio, a Python formatter for LLDB, a Python formatter for GDB. Every team I’ve worked with writes one for their critical types and then watches it go stale as the types evolve.

A tool named reflect-pretty could emit the formatter at build time from your reflected types. It does not exist; this is the command line the sketch imagines:

$ reflect-pretty include/models.hpp \
    --out-natvis  debug/models.natvis \
    --out-lldb    debug/models.lldb.py \
    --out-gdb     debug/models.gdb.py

Output a tool like that would aim for (illustrative, produced by no tool):

(lldb) p user
(User) { name="Filip" age=40 admin=true home=Address{city="Warsaw" postal=12345} }

With such a tool, you would change a struct, re-run it, and the formatter would follow.

Papercut #2: documentation

Doxygen is 30 years old and still the standard. Its output looks 30 years old too. Modern alternatives (Sphinx-cpp, Hugo-doc) mostly parse Doxygen XML under the hood.

The real problem: Doxygen doesn’t know about your annotations, the [[=json_name("id")]] that names the JSON wire field, the [[=gen::range(0, 150)]] that constrains property test inputs. Those are where the actual semantic information lives in a reflection-driven codebase, and Doxygen can’t see them.

A tool named reflect-docs could walk your types with reflection, extract identifiers and annotations (reflection cannot see comments), and emit modern markdown / HTML. It does not exist; this is the command line the sketch imagines:

$ reflect-docs include/models.hpp --out docs/api/

It would produce docs/api/user.md, shown here as an illustration of the target (produced by no tool):

# `struct User`

Creates a new user account. (from `[[=description("...")]]`)

| Field | Type | Constraints | Description |
|---|---|---|---|
| `name` | `std::string` | — | Display name |
| `age`  | `int` | `gen::range(0, 150)` | — |
| `email`| `std::string` | `gen::regex("...")` | — |
| `admin`| `bool` | — | — |

## JSON wire format

```json
{
  "name":"filip",
  "age":40,
  "email":"filip@example.com",
  "admin":false
}

(would be generated from a JSON serializer’s schema; there is no reflect_json repository)


Your `struct` declaration is your documentation source of truth.

## How the build step works

> **Not possible as described.** `std::meta` functions run inside the compiler, while it compiles the program that calls them. A separate binary cannot use a compiler frontend as a library to evaluate them over a header, and `namespace_members_of` is not a `std::meta` function. The working shape is a generator program that includes the header and walks `std::meta::members_of(^^ns, ctx)`, as the correction at the top says. The list below is the design as first written.

`reflect-pretty` and `reflect-docs` were meant to be thin binaries built on top of the clang-p2996 compiler frontend as a library. When you run `reflect-pretty models.hpp`, it would:

1. Invokes clang-p2996 frontend to parse `models.hpp`.
2. Walks every top-level struct/class reflection (`namespace_members_of(^^your_namespace)`, which is not a real function; see the note above).
3. For each type, generates an emitter snippet in the target format:
   - natvis: XML with `<DisplayString>` + `<Expand>` blocks.
   - LLDB: Python class subclassing `lldb.SBSyntheticValueProvider`.
   - GDB: Python class subclassing `gdb.printing.PrettyPrinter`.
4. Writes the output file.

The generated Python/XML files would be small, so you could check them in, review diffs, rerun in CI.

## Walking for pretty-printers (sketch)

```cpp
// Sketch, refused by GCC 16.2.0 when the result is used at run time: a
// consteval function cannot return std::string out of constant evaluation.
// The embedded demo returns char const* through std::define_static_string.
template <typename T>
consteval std::string natvis_snippet() {
    std::string out;
    out += "<Type Name=\"";
    out += std::meta::display_string_of(^^T);
    out += "\">\n  <DisplayString>{" ;
    constexpr auto ctx = std::meta::access_context::unchecked();
    bool first = true;
    template for (constexpr auto m
                  : std::define_static_array(
                      std::meta::nonstatic_data_members_of(^^T, ctx))) {
        if (!first) out += " ";
        first = false;
        out += std::meta::identifier_of(m);
        out += "={";
        out += std::meta::identifier_of(m);
        out += "}";
    }
    out += "}</DisplayString>\n  <Expand>";
    template for (constexpr auto m
                  : std::define_static_array(
                      std::meta::nonstatic_data_members_of(^^T, ctx))) {
        out += "\n    <Item Name=\"";
        out += std::meta::identifier_of(m);
        out += "\">";
        out += std::meta::identifier_of(m);
        out += "</Item>";
    }
    out += "\n  </Expand>\n</Type>";
    return out;
}

One function walks the fields and emits the visualisation. A tool could compose this over each struct in a header to produce a .natvis file; that tool does not exist.

Comparison

Tool Source of truth Manual per-field writing Stays in sync
Hand-written natvis .natvis files Yes No
Visual Studio native debug info No Partial (works for primitives)
LLDB / GDB pythons .py files Yes No
Rust #[derive(Debug)] The struct No (auto) Yes
reflect_dx (the sketch in this post, not built) The struct No (reflection) Yes (re-run on change)

Why both tools in one repo

The design assumed that reflect-pretty and reflect-docs would share their plumbing, a frontend wrapper around clang-p2996 that exposes reflection to a host program. That wrapper is the part that does not work as described, and neither tool is built, so no share of shared code was measured. The idea of a third tool, reflect-bindings, which would emit Python / Lua / WASM bindings from a C++ header, is also only an idea.

What exists today

github.com/wrocpp/reflect_dx is a placeholder: a README with no code, licence or release. The embedded demo is the only code. It walks the data members of Address and User and prints a .natvis <Type> element for each, with a DisplayString and an Expand block. It has no command-line tool, no LLDB or GDB formatter, no documentation generator, no CMake integration and no output for STL types. The .natvis text it prints was not loaded into Visual Studio here.

Next in the reflection arc