Ch24.4: Support and Adoption

中文(简体)

1. What Actually Implements It

Reflection is in C++26, but "in the standard" is not the same as "in your compiler". The support table at the time of writing:

Implementation Reflection Notes
GCC 16 Yes, partial library support The first mainstream compiler with a C++26 reflection implementation. The language features and most of <meta> are present; some library facilities and corner cases are still being completed. Use -std=c++26 and check the release notes for your exact build.
Bloomberg clang-p2996 Yes, reference implementation A fork of Clang maintained alongside the proposal. It is the implementation used to demonstrate many P2996 examples. It is not upstream Clang; do not assume the same flags or the same completeness.
EDG Yes, implementation available EDG's front end implements the proposal and is available on Compiler Explorer. It is a front end, not a complete toolchain you can drop into a normal build.
MSVC No public implementation At the time of writing there is no public reflection implementation in MSVC. Code that uses reflection does not build there.
Upstream Clang No complete implementation Reflection has not landed as a complete feature in upstream Clang. Check the current compiler support table before relying on it.

The practical consequence: reflection is ready for experiments, internal tools, and projects that can pin a compiler. It is not yet a feature you can put in a library that must build on every compiler your users have.

2. Feature-Test Macros

The feature-test macros tell you whether the language and the library are present:

#if defined(__has_include)
#  if __has_include(<meta>)
#    include <meta>
#  endif
#endif

#if defined(__cpp_impl_reflection) && __cpp_impl_reflection >= 202506L && \
    defined(__cpp_lib_reflection) && __cpp_lib_reflection >= 202506L
#  define HAS_CXX26_REFLECTION 1
#else
#  define HAS_CXX26_REFLECTION 0
#endif

Do not guess from the compiler name. A vendor may ship a partial implementation, and a fork may define the macro while missing a function you need. Guard the language feature and the library feature separately, and guard the specific facility if it has its own macro.

A feature-test macro is a promise about the feature, not a promise about every corner case. Test the code you actually use. A small compile-only test in CI is worth more than a matrix of version numbers.

3. A Portability Boundary

The clean way to adopt reflection is to isolate it behind one header and one interface:

reflection/
  detect.h        // feature macros, HAS_CXX26_REFLECTION
  enum_names.h    // enum_to_string, string_to_enum
  json.h          // generic serializer
  json_fallback.h // hand-written or macro-based implementation
  api.h           // includes one of the two, same public names

The rest of the program includes api.h and never sees ^^ or [: :]. On a compiler with reflection, the implementation is generated. On a compiler without it, the fallback is the old hand-written or macro-based code. The public API is identical, so the rest of the program does not care.

This boundary also contains the compile-time cost. A header that reflects every type in the project slows every translation unit that includes it. Keep the reflection-heavy code in a small number of translation units, and expose ordinary functions or types to the rest of the program.

Do not put reflection in a public ABI. A reflection value is a compile-time handle; it is not a stable binary interface. Generated aggregates are fine in an ABI if their layout is stable, but the reflection machinery that produced them is an implementation detail.

4. What to Use Today

If you need the functionality now and cannot pin a reflection-capable compiler, the existing tools still work:

Tool What it does Limit
Boost.PFR Aggregate field access and tuple-like iteration without macros Aggregates only; no names, no enum reflection, no type generation
magic_enum Enum names and parsing by scanning the compiler's own diagnostics Compiler-specific tricks, limits on enum size and flags
Macro tables (X-macros) One list expands into declarations and visitors Textual, no type information, still a second declaration
External generators Parse headers or schemas and emit C++ Build step, another parser, stale output when the input changes
Compiler plugins Full access to the AST Compiler-specific, fragile across versions, hard to distribute

These are not wasted skills. A reflection-based implementation still needs the same policy decisions, and the fallback path is often one of these tools. The difference is who writes the member list: you or the compiler.

5. An Adoption Plan

Do not start by rewriting the serializer. Start with the smallest place where duplication hurts and a failure is easy to detect:

  1. Enum names. Replace a hand-written to_string switch. The generated function is easy to compare with the old one, and a missing enumerator is a compile-time or test-time failure.
  2. Debug printing. Generate a field printer for internal types. It is not on the hot path, and the output is easy to inspect.
  3. Serialization for a bounded format. JSON or a configuration file. Keep the policy layer explicit: escaping, optional fields, versions, unknown keys.
  4. Bindings or a schema. Generate the repetitive wrapper from the declaration only after the first three steps have proven the toolchain stable in your build.
  5. Public API. Only when every compiler you support has a complete implementation, or when the reflection code is hidden behind a non-reflective ABI.

At every step, keep a fallback. The cost of a fallback is small compared with the cost of discovering that one supported compiler cannot build the release.

6. What "Partial" Means in Practice

A partial implementation usually fails in one of these places:

The mitigation is the same for all four: keep the reflection code small, test it in isolation, and have a fallback for the facility that is missing. Do not build a reflection-heavy framework on a partial implementation and then discover the gap in a release build.

7. The Direction of the Language

Reflection for C++26 is deliberately a core, not the final form. The proposal itself says that more powerful code injection and generation are desirable directions. The pieces that shipped are the ones the committee could specify and implement:

That is already enough for serialization, enum names, bindings and layout checks. The absence of general code injection is why define_aggregate only produces simple aggregates. Future standards may widen the generation side; the mental model — reflect, query, splice — will not change.

8. Further Reading

Key Takeaways

That is the chapter. The language model is three operators and a library; the engineering work is choosing where to put the boundary and how to fall back.