Nihilai Collective Logo

OACC

Order-Agnostic Constexpr Configuration with Compile-Time Uniqueness Checking

Whitepaper · Version 1.0 (Revised) · Jun 2026

Abstract

OACC is a C++20/23 configuration pattern. Each setting is wrapped in its own type; a variadic consteval generator accepts those wrappers in any order, routes each to the right field by overload resolution, and uses a concept to reject calls that pass the same setting type twice. Under consteval the whole thing evaluates during compilation and leaves no runtime trace — field values appear as immediates in the output assembly. None of the ingredients are new: strong type wrappers, consteval member updates, fold expressions, and concept constraints are all standard C++20. The contribution is the combination — order-independent configuration with compile-time duplicate rejection — plus one non-obvious implementation detail that makes or breaks the guarantee: the uniqueness concept must constrain the argument pack as a whole, not each argument individually. Applied per-argument it compiles, checks nothing, and silently allows duplicates through.

1. Background

Configuration with many independent parameters (5–20+) wants several things at once: type safety, freedom to pass arguments in any order, early detection of mistakes like passing the same setting twice, and no runtime cost. The common approaches each give up one of these.

Positional arguments are concise but order-sensitive, unreadable at the call site, and easy to transpose two arguments of the same type. The builder pattern is order-independent and readable, but a repeated setter is last-write-wins — a silent bug, detectable only at runtime if at all. Designated initializers (C++20) are readable and reject duplicate fields at compile time, but the field order must match the struct declaration and there is no place to attach cross-field validation. Named parameters (P0671) are the proper language-level answer and would be strictly better, but they are not in the standard — targeting C++26 at the earliest.

These are trade-offs, not disqualifications. OACC is worth its machinery specifically when you have many parameters, want order independence, and want a duplicate to be a build error rather than a silent overwrite — in C++20, without waiting for a language feature.

Cathedral Architecture (see Philosophy) treats compile-time-known values as first-class: anything that is fixed before a request runs should not cost anything at runtime, and mistakes that are detectable at compile time should not wait for runtime to surface them. OACC applies both principles to configuration.

2. The Pattern

2.1 Strongly-Typed Wrappers

Each setting is its own type, so the type system can route it. Scoped enums work well as value carriers:

wrapper types
enum class exceptions_type : bool { disabled = false, enabled = true }; enum class max_batch_size_type : uint64_t {}; enum class max_context_length_type : uint64_t {}; enum class dev_type : bool { disabled = false, enabled = true };

2.2 One Update Overload Per Setting

The config struct holds the fields and a consteval update() overload per wrapper type, each selected by std::same_as so overload resolution picks the right one:

config struct with update overloads
struct model_config { exceptions_type exceptions{}; max_batch_size_type max_batch_size{ static_cast<max_batch_size_type>(1) }; max_context_length_type max_context_length{ static_cast<max_context_length_type>(1024) }; dev_type dev{}; template<std::same_as<max_batch_size_type> V> consteval auto update(V v) const { auto r{*this}; r.max_batch_size = v; return r; } // … one overload per wrapper type … };

Because each update writes one field and reads none of the others, applying them in any order yields the same result — the operations commute. That is the basis of the order-independence claim, and it depends on keeping the updates genuinely independent (see Section 4.2).

2.3 The Generator — and the Critical Detail

This is the load-bearing part. The concept must constrain the argument pack as a whole via a requires clause:

uniqueness concept and generator
template<typename search_type, typename... check_types> constexpr uint64_t type_occurrence_count = (static_cast<uint64_t>(std::is_same_v<std::remove_cvref_t<search_type>, std::remove_cvref_t<check_types>>) + ...); template<typename... arg_types> concept unique_configuration_types = ((type_occurrence_count<arg_types, arg_types...> == 1) && ...); template<typename... arg_types> requires unique_configuration_types<arg_types...> consteval auto generate_model_config(arg_types... args) { model_config config{}; ((config = config.update(args)), ...); return config; }

If you instead write template<unique_configuration_types... arg_types>, you constrain each argument individually — which is always satisfied, since a type trivially appears once among only itself — and duplicates are not caught. The whole-pack requires form is what makes the guarantee real. Verify your own call site rejects a duplicate rather than assuming it does.

3. Verified Behavior

Everything in this section was checked on GCC 13.3.0, x86-64, -std=c++23. The pattern uses only standard C++20 features; reproduce on your target toolchain if it matters.

3.1 Order Independence

Two calls with the same settings in different orders produce the same configuration. This static_assert holds:

order independence — compiles and passes
static constexpr auto a = generate_model_config(dev_type::enabled, max_batch_size_type{23}); static constexpr auto b = generate_model_config(max_batch_size_type{23}, dev_type::enabled); static_assert(static_cast<uint64_t>(a.max_batch_size) == static_cast<uint64_t>(b.max_batch_size) && a.dev == b.dev);

3.2 Duplicates Are Rejected at Compile Time

Passing the same wrapper type twice makes the call non-viable. Verbatim GCC 13.3 output:

duplicate — compile error
error: no matching function for call to 'generate_model_config(max_batch_size_type, dev_type, max_batch_size_type)' note: candidate: 'template<class ... arg_types> requires unique_configuration_types<arg_types ...> consteval auto generate_model_config(arg_types ...)' note: constraints not satisfied note: required for the satisfaction of 'unique_configuration_types<arg_types...>' [with arg_types = {max_batch_size_type, dev_type, max_batch_size_type}]

The diagnostic names the failing function, the unsatisfied concept, and the exact argument pack — including the duplicated max_batch_size_type. If you want the literal occurrence count surfaced in the message, route the check through a static_assert helper that embeds the count as a template value argument; the bare concept gives you the pack, which is usually enough to see the problem.

3.3 Complete Erasure Under consteval

When the generator and updates are consteval and the arguments are constants, the configuration is computed at compile time and nothing remains at runtime. For a program that builds a config and prints one field, the entire body of main at -O2 is:

emitted assembly — main body only
movl $23, %edx ; the computed field value, already a literal movl $2, %edi xorl %eax, %eax leaq .LC0(%rip), %rsi call __printf_chk@PLT xorl %eax, %eax ret

There is no trace of update, generate_model_config, or the model_config struct — the field value appears as the immediate $23. The configuration system has zero runtime footprint in this context.

3.4 Scope of the Zero-Overhead Claim

Context Where it evaluates Runtime cost
consteval generator, constant args Compile time only None — fully erased (verified)
constexpr, used in a constant expression Compile time None
constexpr, called with runtime values Runtime Small; typically inlined at -O2, not guaranteed
Plain runtime (no constexpr) Runtime Struct copies through the update chain

The uniqueness check itself is always free at runtime — it is a concept, evaluated during overload resolution, and never emits code regardless of context. The erasure of the configuration values is what depends on consteval / constant evaluation.

4. Discussion

4.1 The One Way to Get It Wrong

The entire duplicate-rejection guarantee rests on a single syntactic choice. requires unique_configuration_types<arg_types...> passes the whole pack to the concept and the fold inside counts occurrences across all of them. template<unique_configuration_types... arg_types> applies the concept to each argument in isolation — each type trivially appears exactly once among only itself — so the concept is always satisfied and duplicates pass through silently.

Both forms compile. Neither produces a warning. The only way to know which you have is to test that a duplicate actually fails to compile. Test it once, per toolchain, before shipping. This is the pattern's one sharp edge.

4.2 When Order Starts to Matter

Order independence holds precisely because each update writes one field and reads none of the others — the operations commute. If an update reads another field (for example, clamping generation length to context length at update time), the operations no longer commute and the order of arguments changes the result. Keep updates field-local. If cross-field validation is needed, do it after the generator returns, as a separate step on the completed config.

4.3 vs Named Parameters (P0671)

Named parameters would be strictly better at the call site — generate_model_config(max_batch_size: 23, dev: true) reads more clearly than generate_model_config(max_batch_size_type{23}, dev_type::enabled). They are the proper language-level answer. OACC delivers the safety properties — order independence plus duplicate rejection — in C++20 today. When P0671 lands, the readability case for migrating to it is strong; the uniqueness semantics would need to be re-verified against whatever the language spec says about duplicate named arguments.

4.4 Why Not Designated Initializers?

Aggregate initialization with designated initializers (Config{ .a = …, .b = … }) is simpler, also rejects duplicate fields at compile time, and has no boilerplate. It is the right tool when the parameter set is small, the struct layout is stable, and you don't need order independence or composition. OACC adds exactly two things designated initializers don't have: freedom to supply arguments in any order, and a place to attach computed defaults or cross-field logic in the update overloads. If you don't need either, use aggregate init.

5. Comparison

Pattern Order-independent Duplicate detection Runtime cost (constant case) Available
OACC Yes Compile-time (build error) None under consteval (verified) C++20
Positional args No Cannot express None Always
Builder Yes Runtime, or silent last-write-wins Builder object unless optimized Always
Designated initializers No (struct order) Compile error, terse message None C++20
Named params (P0671) Yes Compile-time None C++26+ (proposed)

6. Limitations

  • The constraint must be a requires clause over the pack. As shown in Section 2.3, constraining each argument individually silently disables duplicate detection. This is the one place the pattern is easy to get wrong — test that a duplicate actually fails.
  • One wrapper type and one update overload per setting. This is the pattern's boilerplate. C++26 reflection could generate it; today it is written by hand.
  • Every parameter needs a distinct type. Two raw uint32_t arguments are ambiguous and defeat the routing; wrap them (enum class width : uint32_t {} / height). This is also a feature — it prevents transposing same-typed arguments — but it is real work.
  • Order independence assumes independent updates. If an update reads another field, the operations no longer commute and order matters again. Keep updates field-local, or sequence dependent steps explicitly and validate after.
  • "Zero overhead" is the consteval/constant case. Verified there. Runtime use with non-constant values is efficient-if-inlined, not guaranteed-free.
  • Verified on GCC 13.3 only. Standard C++20 throughout; reproduce on your target toolchain.

7. Reproducibility

The full implementation and test harness are available at github.com/nihilai-collective. Three things are worth verifying directly on your toolchain: that the order-independence static_assert in Section 3.1 holds, that the duplicate call in Section 3.2 fails to compile, and that the assembly output in Section 3.3 matches. All three are small, self-contained checks that take under a minute to run.

8. Conclusion

OACC is a practical assembly of standard C++20 features into a configuration interface with three properties: order-independent arguments, compile-time rejection of duplicate settings, and — under consteval — complete erasure of the configuration machinery, leaving the field values as literals in the output. The first two were verified by compiling valid and invalid calls and reading the diagnostics; the third by inspecting the emitted assembly.

The single subtlety that makes or breaks the pattern is applying the uniqueness concept to the whole argument pack through a requires clause. Applied per-argument it compiles, checks nothing, and silently allows duplicates through. Where a simpler tool fits — few parameters, fixed order, or runtime checking being acceptable — this machinery is not warranted, and we have said where those lines fall.

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

References

  1. ISO/IEC 14882:2020. Programming Languages — C++. (consteval, concepts, fold expressions, designated initializers.)
  2. Stroustrup, B. Named Arguments for C++. P0671R2. 2018. wg21.link/p0671r2
  3. Niebler, E. Strong Typedefs. C++ Now 2016. (Motivating use case for scoped-enum value wrappers.)
  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.