Nihilai Collective Logo

CAFBERIHT

Zero-Overhead Heterogeneous Collections in C++20/23

Whitepaper · Version 1.0 (Revised) · Jun 2026

Abstract

CAFBERIHT is a C++20/23 pattern for holding a fixed set of heterogeneous types in one object, iterating over them, applying operations to only the subset that supports them, and accessing any of them by index or enum — all resolved at compile time, with no virtual dispatch, no type erasure, and no function-pointer indirection. The pattern is built from CRTP, variadic multiple inheritance, pack using-declarations, fold expressions, and if constexpr filtering. The contribution is the specific combination, plus a CRTP base (core_elem_base) that collapses what would otherwise be four operator[] overloads per component into a single inheritance line. At -O1 and above, the dispatch machinery produces instruction-for-instruction identical output to a hand-written baseline — verified by diff on compiler-emitted assembly, not by assertion.

1. Background

Sometimes you need to hold a collection of related-but-different types and operate over them uniformly without giving up their distinct types or paying for runtime polymorphism. The standard options each carry a trade-off.

std::variant and type erasure (std::any, void*) give runtime indexing and heterogeneous visitation, but add a runtime discriminator and, for any, lose static type information. Virtual hierarchies are clean and extensible, but every object carries a vtable pointer, every call is an indirect jump, and all types are forced through one base interface. std::tuple is zero-overhead and fully typed, but applying an operation across elements requires every element to support it, and access is positional rather than semantic. Type-list libraries like Boost.Mp11 and Brigand are powerful for compile-time type computation but are not oriented toward holding runtime state with ergonomic access.

CAFBERIHT occupies the same niche as std::tuple, with two differences that matter for some workloads: operations can be filtered so that only matching components are touched (the others generate no code), and components are addressable by a semantic enum rather than a positional index. The cost is that the set of types is fixed at compile time. These are trade-offs, not verdicts — the alternatives above are different points on the same design space. If your type set is genuinely dynamic, use a different tool.

Cathedral Architecture (see Philosophy) treats any value fixed at construction time as a candidate for compile-time encoding. CAFBERIHT applies that principle to type membership itself: if the set of components a collection will ever hold is known when you write the code, encoding it in the type system is free and eliminates the dispatch cost entirely.

2. The Pattern

2.1 The CRTP Base

The ergonomic centre of the pattern is a CRTP base that gives each component a tag-dispatched operator[]:

core_elem_base
template<auto index> using tag = std::integral_constant<uint64_t, static_cast<uint64_t>(index)>; template<auto index, typename derived_type> struct core_elem_base { constexpr decltype(auto) operator[](tag<index>) & noexcept { return *static_cast<derived_type*>(this); } constexpr decltype(auto) operator[](tag<index>) const& noexcept { return *static_cast<const derived_type*>(this); } };

A component opts in with one line of inheritance:

component opt-in
template<auto enum_value_new> struct core_interface : core_elem_base<enum_value_new, core_interface<enum_value_new>> { uint64_t kernel_iteration_count{}; static constexpr core_types enum_value{ enum_value_new }; };

Without the CRTP base, each component would hand-write the operator[] overloads itself — four overloads per component (lvalue, const-lvalue, rvalue, const-rvalue) instead of one inheritance line. For a collection of N components, that reduction is the practical reason the pattern is usable.

2.2 The Aggregate

The collection inherits from all its components and merges their operator[] overloads into one overload set:

cafberiht aggregate
template<typename... bases> struct cafberiht : public bases... { using bases::operator[]...; static constexpr uint64_t size{ sizeof...(bases) }; };

Because each core_elem_base<index, Derived> is keyed on both the index and the derived type, two components can never share a base subobject. Diamond inheritance cannot be formed in this hierarchy — this is a structural consequence, not a coding convention.

2.3 Filtered Dispatch

Operations are expressed as mixins with a compile-time filter() and an impl(). Applying a mixin folds over the bases, guarding each call with if constexpr:

filtered application
template<template<typename> typename mixin_type, typename... arg_types> constexpr void impl(arg_types&&... args) noexcept { (impl_internal_filtered<mixin_type, bases>(args...), ...); } template<template<typename> typename mixin_type, typename base_type, typename... arg_types> constexpr void impl_internal_filtered(arg_types&&... args) noexcept { if constexpr (mixin_type<base_type>::filter()) { mixin_type<base_type>::impl(*this, args...); } }

Components whose filter() is false are excluded by if constexpr — no code is generated for them. This is the one capability that distinguishes the pattern from a plain tuple: partial interfaces. You can apply an operation to only the components that support it, without requiring a common interface and without a runtime check to skip the rest.

3. Performance

3.1 Method

The performance claim is narrow and testable: under optimizing compilation, code using CAFBERIHT produces the same machine code as the equivalent hand-written sequence of direct calls.

A cafberiht of 10 components has a mixin applied that keeps even-indexed components (5 of 10) and does out += rand() into a volatile accumulator (the volatile prevents the optimizer from folding the calls away, so they survive as observable work). The baseline is ten plain structs, one inherited aggregate, and five literal out += rand(); statements. Both compiled with g++ -std=c++23.

3.2 Result

At -O2, the body of main is instruction-for-instruction identical between the two. Normalizing only the compiler-internal function-label number, diff reports no differences; both main bodies contain 74 lines of assembly. The dispatch portion is exactly five direct call rand@PLT instructions — no loop, no indirect branch, no vtable load. The five odd-indexed components produced zero instructions.

The equivalence holds at -O1 and above. At -O0 the two differ (CAFBERIHT is actually fewer instructions there, because the baseline's repeated volatile accesses aren't coalesced), but -O0 is not a meaningful comparison point — nothing is inlined on either side.

3.3 Across Optimization Levels

-O level CAFBERIHT main insns Hand-written main insns Equivalent?
-O04370
-O14141Yes
-O26464Yes
-O36464Yes

Verified on GCC 13.3.0, x86-64, -std=c++23. The pattern uses only standard C++20/23 features and there is no obvious reason it would behave differently on Clang or MSVC, but we have not run those compilers and do not claim specific results for them. Reproduce with:

reproduce
g++ -std=c++23 -O2 -DCAFBERIHT_WIDTH=10 example.cpp -S -o example.s g++ -std=c++23 -O2 baseline.cpp -S -o baseline.s diff <(awk '/^main:/{f=1} f; /\.cfi_endproc/{if(f)exit}' example.s) \ <(awk '/^main:/{f=1} f; /\.cfi_endproc/{if(f)exit}' baseline.s)

3.4 Compile-Time Scaling

The runtime dispatch cost is zero; the compile-time cost is not. The following figures were measured on an Intel Core i9-14900KF (full template instantiation plus codegen, single translation unit):

Compiler N = 100 N = 1,000 N = 10,000
GCC 14.2.0 (WSL/Linux x64)0.96 s4.90 s54 m 38 s
Clang 19.1.1 (WSL/Linux x64)1.12 s3.26 s3 m 37 s
MSVC 19.44.35222 (Windows x64)1.30 s3.61 s4 m 38 s
Compile time vs. component count across GCC, Clang, and MSVC

In the intended range the cost is a non-issue. At N = 100 every compiler is at roughly one second; below ~1,000 components the scaling is actually sub-linear (apparent exponent ~0.4–0.7), because fixed compiler startup dominates. Past ~1,000 components it degrades sharply: Clang and MSVC approach quadratic (~1.8–1.9 exponent), and GCC is markedly worse (~2.8, roughly cubic), blowing up to nearly an hour at 10,000. The likely mechanism is the using bases::operator[]...; overload set: merging N tag-dispatched operator[] declarations into one set is a known super-linear cost centre in current compilers. This pattern is for collections of up to a few hundred components. That covers every intended use case comfortably. The 10,000-component figures are a stress test that locates the ceiling, not a deployment recommendation.

4. Structural Invariants

These are properties enforced by the type system, stated as facts about the construction rather than as theorems.

  • Index uniqueness. Two components sharing the same tag<index> make operator[](tag<index>) ambiguous, which is a compile error. Indices must be distinct.
  • No shared base subobject. core_elem_base<i, A> and core_elem_base<i, B> are different types whenever A and B differ, because the base is parameterized on the derived type as well as the index. No two components can share a base subobject.
  • Diamond inheritance is unrepresentable. A diamond requires a common base reachable by two paths. Since no two components share a base subobject, the inheritance graph has no such common base — the classic multiple-inheritance hazards cannot arise in this hierarchy by construction.
  • Empty-base optimization applies. A 10-component aggregate where each component holds one uint64_t has sizeof == 80. The CRTP bases contribute zero bytes — confirmed by static_assert(sizeof(C) == 10 * sizeof(uint64_t)) on GCC 13.3.
  • Compile-time bounds checking. Out-of-range index access is rejected at compile time, and the diagnostic names the offending value in the error message — no separate debugging pass required.

5. Comparison

Approach Static typing Dispatch cost Filtered ops Semantic access Membership
CAFBERIHT Full, compile-time None (verified) Yes Index + enum Fixed at compile time
std::tuple Full, compile-time None All or nothing Positional get<I> Fixed at compile time
std::variant Active type runtime Visitation dispatch Via visit Yes One-of-set, runtime
Virtual hierarchy Erased to base vtable + indirect call Common interface only Yes Open, runtime
Boost.Mp11 / Brigand Full, compile-time None Type-level Not designed for runtime state Fixed at compile time

The honest one-liners: vs std::tuple — CAFBERIHT adds filtered application and enum-based semantic access; if you don't need either, a tuple is simpler and you should use it. Vs std::variant — different problem; variant is for one-of-a-set chosen at runtime. Vs virtual hierarchy — CAFBERIHT trades runtime extensibility for the elimination of vtable overhead. Vs Mp11/Brigand — those are type-computation toolkits; this is a runtime-usable container that happens to resolve its structure at compile time.

6. Limitations

  • Fixed membership. All types are known at compile time. For runtime-variable sets, wrap a small number of concrete cafberiht instantiations in a std::variant, or use a different pattern entirely.
  • Requires C++20+. Fold expressions, if constexpr, pack using-declarations, and concepts for legible error messages.
  • Compile-time cost is super-linear past ~1,000 components. In the intended range this is negligible. Well outside it, it is not. Keep collections small; alias common instantiations; consider extern template.
  • Debugger ergonomics. Deeply nested template type names are verbose under a debugger. Type aliases for common instantiations help significantly.
  • No built-in thread safety. The aggregate is a plain object; concurrency is the caller's responsibility.
  • The performance claim is conditional. It holds at -O1+ and was verified only on GCC 13.3. At -O0, or on a toolchain that fails to inline the helpers, the equivalence is not guaranteed. Verify on your target.

7. Reproducibility

The full implementation and example harness are available at github.com/nihilai-collective. The assembly diff test is a small, self-contained benchmark — build example.cpp and baseline.cpp against a C++23-capable GCC installation and run the diff command in Section 3.3. All compile-time scaling measurements were taken with the harness and hardware described in Section 3.4 and can be reproduced by varying the -DCAFBERIHT_WIDTH compile definition.

8. Conclusion

CAFBERIHT is a practical assembly of established C++20/23 techniques into a heterogeneous container with three useful properties: full static typing, compile-time filtered dispatch over partial interfaces, and enum-based access — at no dispatch cost relative to hand-written calls, under optimizing compilation. The boilerplate that would normally make this approach unpleasant is reduced to one inheritance line per component by the core_elem_base CRTP helper.

The claims are scoped deliberately. The performance equivalence is demonstrated by diffing compiler output, holds at -O1 and above, and was checked on GCC 13.3 only. The structural guarantees follow from the construction and were each verified to compile and measure as described. Where the pattern is the wrong choice — dynamic membership, runtime-loaded types — we have said so.

"Whatever's already known when you write the code, let the compiler act on it." — Cathedral Architecture

References

  1. Vandevoorde, D., Josuttis, N. M., and Gregor, D. C++ Templates: The Complete Guide, 2nd ed. Addison-Wesley, 2017. (CRTP, variadic templates, fold expressions.)
  2. ISO/IEC 14882:2020. Programming Languages — C++. (C++20 if constexpr, pack using-declarations.)
  3. Niebler, E. et al. A Unified Executors Proposal for C++. P0443. (Motivating use case: heterogeneous executor sets with partial-interface dispatch.)
  4. Nihilai Collective. Cathedral Architecture: An Overview. 2026. /philosophy/
Philosophy
The methodology this pattern applies. Cathedral Architecture in full.
All Papers
Whitepapers and technical write-ups from the Nihilai Collective.