Article

C++26 Reflection Moves Dispatch-Table Registration to Compile Time

A C++26 static-reflection implementation builds a name-to-function dispatch table at compile time, removing a separately maintained registry and runtime table initialization. Lookup, indirect-call costs, naming constraints, and toolchain limitations still remain.

Share

Koharu's reading tip

Separate the eliminated initialization work from the lookup that still happens at runtime. That distinction makes it much easier to decide where this technique fits.

Koharu's reading tip

A dispatch table is a familiar way to map a string or enum value to a function. In C++, however, adding a handler often means updating a separate registry as well, or constructing a container during program startup.

In August 2026, the ISO C++ Blog highlighted a different design based on C++26 static reflection. The compiler discovers eligible functions and builds a fixed table during compilation.

Does the phrase “zero runtime overhead” mean that lookup and function calls become free? Following the mechanism makes the boundary much clearer: some work disappears, some remains, and current toolchain support still limits where the design can be adopted.

Function names replace a separately maintained registration list

Traditional C++ dispatch tables commonly use an if-else chain, X-macros, or a container such as std::unordered_map populated with function pointers. Each approach works, but it either maintains a name-to-function list separately from the handlers or performs registration at runtime.

The rbox implementation article builds the mapping from a namespace and a naming pattern. The call site is compact:

C++
namespace ops {
int do_add(int x, int y);
int do_sub(int x, int y);
int do_mul(int x, int y);
}

constexpr auto dispatchTable = RBOX_FUNCTION_FIXED_MAP(ops, "do_*");

do_add becomes the key "add", while do_sub becomes "sub". Adding another matching function to the namespace no longer requires copying its name into a second registry.

The removed boilerplate is the handwritten entry list. The macro, naming convention, common function signature, and library dependency do not disappear. Instead, the naming convention becomes a compile-time contract that determines which functions are exposed.

^^ and std::meta::members_of turn names into function pointers

The foundation is P2996R13, Reflection for C++26. A reflection expression such as ^^ops produces a std::meta::info value representing a namespace, type, or another program entity.

The table-building process can be understood in five steps:

  1. Reflect the target namespace with ^^ops
  2. Enumerate its direct members with std::meta::members_of
  3. Read identifiers with std::meta::identifier_of and retain names matching do_*
  4. Check types with std::meta::type_of and obtain function pointers with std::meta::extract
  5. Build the fixed key-pointer mapping during constant evaluation and materialize static data usable at runtime

std::meta::info is not a general runtime metadata object. It is a consteval-only type, so the structural inspection finishes during compilation and the executable retains the fixed data needed for lookup.

There are boundaries. members_of reports direct members of the selected namespace or class; it does not automatically recurse into child namespaces or base classes. Class queries also interact with access context. The registration scope therefore needs to be designed together with the namespace or class layout.

Table construction disappears, but lookup and indirect calls remain

Compared with inserting entries into a std::unordered_map at runtime, this design removes fixed-table construction from program startup. The program can use already generated data while avoiding a second manually maintained list.

The runtime still has to find a key and call through the resulting function pointer. The current rbox README says it selects a dense array, binary search, or hash table according to the input. The lookup cost of the selected representation does not become zero.

“Zero runtime overhead” is therefore best read as keeping discovery, generation, and initialization of the registry out of runtime, not as making dispatch itself cost-free. A useful evaluation separates startup work, per-lookup latency, indirect-call cost, and binary size.

Reflection also sees entities that exist at compile time. A system that loads plugins and registers new handlers after startup still needs a mutable registry or another dynamic dispatch mechanism. This technique is strongest when the handler set is closed at build time.

Adoption in C++26 has reached experimental GCC 16 support

Reflection did not arrive suddenly. P2996R13 traces the standardization effort back to at least 2003, and the post-Sofia C++ working draft editor’s report N5015 records the June 2025 application of P2996R13 to the working paper.

GCC 16.1, released on April 30, 2026, can enable P2996R13 with -std=c++26 -freflection. The GCC C++26 status page describes C++26 support as experimental and warns that backward compatibility with pre-final implementations is not maintained.

By contrast, the official Clang C++ status page currently lists P2996R13 as No. Being able to select a C++26 language mode does not by itself imply support for static reflection.

The feature is part of C++26 at the standardization level, but portable production use across major compilers is not ready yet. Today, the practical path is to evaluate the design in an experimental GCC 16 environment and expand adoption only as the required toolchains catch up.

Closed handler sets trade registration mistakes for build constraints

The design fits command names, event names, operation names, and other handler sets fixed at build time. Because handler declarations and a separate entry list are not maintained in parallel, omissions and mistyped registry keys become less likely.

Adoption also brings concrete constraints:

  • Keep the naming pattern stable because it defines the exposed set
  • Treat inconsistent function signatures as compile-time failures
  • Pin a reflection-capable compiler and flags in the build environment
  • Place table definitions carefully so expensive constant evaluation does not spread through broad include chains

The final point matters. The implementation article notes that extensive constant evaluation repeated across unrelated translation units can increase compile times. Removing startup work is not a win if too much cost is simply moved into every build.

A sensible experiment starts with one small fixed handler set and compares the conventional and reflection-based versions for maintenance, compile time, lookup performance, and binary output. If dynamic registration or multi-compiler portability is the priority, a constexpr array, X-macro, or runtime container remains a reasonable choice.

C++26 reflection makes handler definitions the single source of truth

The opening question now has a precise answer. C++26 reflection can eliminate duplicated registration code and runtime construction of a fixed table. Key lookup and indirect calls remain, so dispatch itself is not free.

Even with that boundary, using the program’s own names and types as compile-time data is a meaningful design shift. Handler definitions can become the single source of truth. For now, it is best explored in a small, measured scope with an experimental toolchain rather than treated as an immediate wholesale replacement.

Source

Share

Related Articles

These articles share nearby categories or tags, so you can keep reading along the same thread.