A C++ reflection tool built for a seamless experience without macros or UB.
Obligatory self-reflection meta joke.
Minimal CMake setup:
# 3.18.2 is the current project floor for CMake APIs used by the package and
# reflected target integration.
cmake_minimum_required(VERSION 3.18.2 FATAL_ERROR)
project(example LANGUAGES CXX)
find_package(omnirefl CONFIG REQUIRED)
add_executable(example main.cpp)
set_property(TARGET example PROPERTY CXX_STANDARD 20)
# Reflection is not transitive: only this target's own C++ translation units are
# instrumented. Call omni_reflected_target for each target that should be
# reflected.
omni_reflected_target(example)Instrumentation can also be triggered explicitly through <target>.omni
(example.omni for the example target):
cmake --build build -t example.omni#include <omnirefl/functional.hpp>
#include <omnirefl/reflection.hpp>
#include <iostream>
#include <string>
#include <string_view>
struct record {
int foo;
std::string bar;
};
int main() {
using namespace std::string_view_literals;
record value{
.foo = 1,
.bar = "before",
};
std::cout << "before: foo=" << value.foo << " bar=" << value.bar << '\n';
const auto write = [](omni::binding auto b)
// Generic lambdas used as reflected visitors must spell the return type.
-> void {
// `omni::fn::each` is the QoL equivalent of expanding a visitor over a
// tuple with `std::apply`.
omni::fn::each(
[](omni::field_binding auto field) -> void {
constexpr std::string_view name = field.name();
// value() is read-only; ref() exposes a writable reference.
if constexpr ("foo"sv == name)
field.ref() = 8;
// operator* and operator-> are QoL accessors.
if constexpr ("bar"sv == name)
*field = "after";
},
b.public_fields());
};
omni::reflected_call(write, value);
std::cout << "after: foo=" << value.foo << " bar=" << value.bar << '\n';
}The complete example is available in tests/tool/example. The comprehensive guide covers the remaining interface and compatibility features.
- Add
omni_reflected_target(...)for the CMake target. - Use
omni::reflected_call(...)where reflection is needed.
Everything else remains regular C++. Omnirefl discovers the argument types and supported dependencies, then generates and force-includes their metadata. No macros, compiler-specific UB, or manual regeneration are required.
Types can be declared and reflected directly in the same .cpp. No dedicated
declaration headers, schemas, annotations, or checked-in metadata files are
required; generated metadata remains a build artifact.
<omnirefl/functional.hpp> provides compositors such as each, filter,
map, and foldl, including chainable forms:
const auto result = tuple
| omni::fn::filter(predicate)
| omni::fn::map(transform)
| omni::fn::foldl(combine, initial);See the functional tests for detailed examples.
The experimental omni::refl utilities are implemented through code generated
for each reflected type and are available only within reflected scopes:
aggregate_into<T>(fields)shallowly constructs a reflected aggregate by matching field bindings by name. All destination public fields must be present and constructible; additional source fields are ignored. Generated support currently requires an aggregate record without bases or anonymous aggregate members, and excludes unions. See the example.
Omnirefl reflects the public data surface of named C++ records and enums (see Limitations).
- Language: C++11 through C++23; C++20 concepts provide the most ergonomic interface.
- Reflectable declarations:
- named namespace-scope records (structs, classes, and unions) and enums
- nested named records and enums inside non-template records
- unconstrained primary record templates with type, non-type, and template-template parameters, including type packs and CRTP bases
- non-aggregate records and records without a default constructor, when
supplied as existing objects or queried through
omni::type<T>
- Type metadata:
- type names with and without enclosing namespace qualification, entity kind,
and documentation extracted from Doxygen-style leading and trailing
comments:
///,//!,/** */,/*! */,///<, and//!< - qualified names retain enclosing record and namespace identifiers, including those of inline namespaces
- records additionally expose
has_bases()and generatedis_aggregatable()capability queries
- type names with and without enclosing namespace qualification, entity kind,
and documentation extracted from Doxygen-style leading and trailing
comments:
- Public field metadata and access:
- an ordered tuple of public non-static fields, including fields inherited transitively through public bases; hidden and ambiguous inherited fields are omitted
- field name, type names preserving declaration spelling such as alias
templates and
decltype, with and without enclosing namespace qualification, an index local to the declaring record, documentation, and const/mutable/volatile/deprecated traits - read access, consuming move access, writable-field assignment, and safe reference, dereference, and member access
- value/reference capability queries for generic field handling
- bitfield and misaligned packed scalar members remain readable; writable members remain assignable but do not expose references
- private/protected fields, fields inherited through non-public bases, and member functions are omitted
- Enum metadata: enumerator names and values in declaration order.
- Invocation and bindings:
reflected_callvalue arguments produce non-owning bindings;omni::type<T>requests metadata without constructingT- one visitor can receive multiple value and type arguments
- value bindings preserve const/volatile and lvalue/rvalue qualification; visitor value and reference returns are preserved
omni::reflected(...)andis_reflected<T>query generated dependency metadata from inside the visitor
Additional reflected types are discovered through:
- public field types
- public bases and transitive public bases
- public fields of primary template records
- supported public member aliases:
error_typefirst_typekey_typemapped_typesecond_typetypevaluevalue_type
- template-pack routes named
tupleorvariant
Supported public routes may expose otherwise non-public nested dependencies.
Standard-library record types are not traversed as reflectable records outside those protocol routes.
omni_reflected_target(...) is a convenience wrapper; omnirefl itself does not
require CMake:
# Cosmopolitan packages use omnirefl on Unix and omnirefl.exe on Windows.
flags="-std=c++20 -I/path/to/omnirefl/include"
omnirefl -o example.omnirefl.hpp -c main.cpp -- c++ $flags
c++ $flags -include example.omnirefl.hpp main.cpp -o example && ./example-c selects the instrumented source; compiler output options after -- are
ignored. ccdb_query prints the matching command from a compilation database;
the optional final argument selects among commands by output-path substring:
ccdb_query build/compile_commands.json "$PWD/main.cpp" example.dir- Release archives:
Latest release or all releases. Linux packages use.debor.tar.gz; Windows packages use.zip. The experimental Cosmopolitan.tar.gzpackage supports Linux, macOS, and Windows. - Latest CI artifact (if available):
Open the latest successfulCIworkflow run onmasterand download the package artifact for the required runtime and architecture. Artifacts are temporary; cancelled or partially rerun workflows and artifact expiration may leave no downloadable package. - Build locally:
Use the prepared Docker images; see Build Packages Locally.
Install a .deb normally. Unpack a .tar.gz or .zip archive and use its
omnirefl-* directory as the installation prefix.
Several declaration-shape constraints below follow from the generated-header model: reflected types must be nameable before their source declarations. See How It Works.
reflected_callis the instrumentation boundary. The visitor must be either a generic lambda or a type with a templatedoperator(). Its return type must not depend on instantiating the visitor body during the tool run; for lambdas, this means an explicit trailing return type, including-> void. Consequently, a lambda cannot currently return a type declared inside its body.constexpr auto result = reflected_call(...)is not supported: it forces evaluation and breaks that instrumentation boundary.reflected_callaccepts reflected records and enums only. The caller must convert or dispatch other top-level shapes before the call; usestd::visitormpark::visitfor variants. Scalars, pointers, raw arrays, standard-library records, and compound types are not accepted directly. Compound types remain valid dependency routes as listed above. Invalid-input detection is best effort.- A reflected root must be complete and defined before its
reflected_call. - Local and unnamed types are not supported as reflected roots.
- Namespace-scope unscoped enums require a fixed underlying type so the generated header can forward-declare them.
- Records nested inside template records are not supported.
- Public access paths to non-public nested dependencies are not preserved when the exposing field is inherited from a public base. A public nested type inside a private enclosing record is also not currently nameable.
- Records with direct or inherited virtual bases are not supported. They are
rejected as
reflected_callinputs and skipped with a warning when found as dependencies. - Constrained primary record templates and explicit or partial record-template specializations are not supported.
- Direct recursive
reflected_callis not supported inside a reflected scope. A nested reflection call can only work if that reflected path was already instantiated independently. - Reflection queries are valid only inside the reflected scope. The tool reports out-of-scope queries as errors on a best-effort basis.
- Deprecated public fields can emit compiler deprecation diagnostics while
their metadata is formed,
before
is_deprecated()can filter them. - Anonymous unions are not reflected correctly.
- Compiler-packed misaligned raw arrays have no safe whole-field accessor; use
an aligned representation such as
std::arraywhen whole-field access is required. - Pointer/reference pointees and raw-array element types are not dependency routes, regardless of whether their definitions are visible.
- Standard-library public bases are ignored. Other unsupported public bases are skipped with a warning, and their inherited fields are omitted.
omni_reflected_targetdoes not support OBJECT or INTERFACE libraries.- The CMake wrapper instruments concrete, non-generated C++ translation units. Generated sources are skipped, source generator expressions are rejected, and C translation units are ignored. If no C++ source remains, reflection is skipped with a warning.
Packaged test/example sources are available under share/omnirefl/tests. Copy
them into a writable directory before configuring:
# Use /usr for a .deb, or the unpacked omnirefl-* directory for an archive.
prefix=/usr
cp -R "$prefix/share/omnirefl/tests" ./omnirefl-tests
mkdir build && cd build
cmake ../omnirefl-tests -GNinja \
"-Domnirefl_DIR=$prefix/lib/cmake/omnirefl"
ctest --timeout 600 --output-on-failureOn Windows, run from a Visual Studio Developer PowerShell so cl.exe is
configured:
$prefix = "C:\path\to\omnirefl"
Copy-Item -Recurse "$prefix\share\omnirefl\tests" .\omnirefl-tests
New-Item -ItemType Directory build | Out-Null
Set-Location build
cmake ../omnirefl-tests -GNinja `
"-Domnirefl_DIR=$prefix/lib/cmake/omnirefl"
ctest --timeout 600 --output-on-failureThe tests fetch their own test-only dependencies during CMake configuration.
Docker Compose uses prepared, versioned build images. Rebuilding a complete toolchain image locally can take close to an hour.
export PACKAGE_DIR=./artifacts/packages/current
docker compose run --rm build-musl
docker compose run --rm build-musl-aarch64
docker compose run --rm build-ucrt
docker compose run --rm build-cosmoThe Linux package test expects the matching musl archive and the universal
Cosmopolitan archive in PACKAGE_DIR; the commands above populate that
directory.
docker compose run --rm test-alpineThe CI workflow tests these package/platform combinations:
- Linux x86_64 musl and Cosmopolitan packages on Alpine and Ubuntu 18.04, 20.04, and 22.04 with GCC and Clang.
- Linux AArch64 musl and Cosmopolitan packages on Alpine and Ubuntu 22.04 with GCC.
- Windows x86_64 UCRT and Cosmopolitan packages with MSVC, clang-cl, MSYS2 GCC, and MSYS2 Clang.
- The Cosmopolitan package on Intel and Apple Silicon macOS 15 and 26.
The Linux matrix also checks MinGW cross-compilation. Windows AArch64 packaging is not currently supported.
Omnirefl uses a Clang frontend action: it preprocesses the translation unit and builds its AST, but does not perform object-code optimization or code generation. The overhead target is roughly the frontend portion of a complete object build: about 30% as an order-of-magnitude expectation. The actual ratio depends on the source, included headers, compiler, and optimization level.
The packaged benchmark baseline is intentionally large enough to represent a meaningful translation unit and contains a reasonable amount of ordinary and reflected code. CI records its reflection and subsequent Release object-build times across benchmarked platforms. See the continuous benchmark and workflow history for observed results.
Only instrumented targets pay this cost, and reflected translation units can be isolated in dedicated targets. The impact is therefore most noticeable during initial generation. Omnirefl emits dependency files for the source and all its included headers, so Ninja reruns instrumentation only when one of those inputs changes.
CI benchmarks native musl and Cosmopolitan on Linux x86_64, plus Cosmopolitan
on Intel and Apple Silicon macOS. Benchmark inputs use Release;
distributable packages retain RelWithDebInfo for detached symbols.
Reports compare reflection and object-build wall time for benchmark.baseline
against the average of the last five stored runs.
reflected_call identifies root records and enums. Omnirefl walks their public
dependency protocols, then force-includes a generated
header before the translation unit.
The generated header is an internal, per-translation-unit build artifact. It is
not intended to be installed or published as a reusable interface: its metadata
reflects the exact compiler invocation, including preprocessor definitions,
language and target flags, and include paths. The same source may therefore
produce different metadata in another target or project. The header does not
#include user declaration headers or reproduce their definitions.
Earlier iterations attempted to reconstruct the required user includes, but that becomes a separate build-integration problem: a declaration may live only in a
.cpp, a third-party header's supported include path may differ from its filesystem path, and project headers may rely on transitive includes or a particular include order.
The current design avoids guessing. It forward-declares namespace-scope roots where C++ permits it; not every type can be forward-declared (see Limitations). Field access remains dependent on a template parameter, delaying instantiation until the source definition is available. Nested-type lookup uses the same mechanism through SFINAE. A simplified generated shape is:
namespace app {
struct root; // The definition may remain in the translation unit.
}
namespace omni {
namespace detail {
// Field accessors use T, so their instantiation is delayed until app::root is
// complete.
template <typename T>
struct _reflected<struct app::root, T> {
// Metadata omitted.
};
// _wrt means "with respect to": its type is app::root, but remains
// syntactically dependent on T so nested-name lookup is delayed.
template <typename T>
struct _reflected<T,
typename std::enable_if<
std::is_same<T, typename _wrt<app::root, T>::type::nested>::value,
T>::type> {
// Metadata omitted.
};
} // namespace detail
} // namespace omniThis model also defines the declaration boundary. Generated code can reproduce ordinary record and enum forward declarations and defer nested lookup, but it cannot safely recreate local or unnamed types, non-forward-declarable enums, records nested in template records, constrained primary templates, or explicit and partial specializations before their source declarations.
Language servers can report temporary diagnostics because reflected translation
units depend on a force-included generated header. Build the affected source or
refresh it through the <target>.omni target.
Invalid C++ in an instrumented translation unit is reported as a Clang error. Compiler warnings are not reported by omnirefl.
Report defects through
GitHub Issues. For tool crashes
on Linux, please include the command line, stderr/stdout, the input .cpp, the
generated header if one was produced, and a backtrace.
# Enable core dumps for the current shell, then rerun the exact failing command.
binary=./omnirefl # Use ./omnirefl.exe for the Cosmopolitan APE payload.
ulimit -c unlimited
"$binary" -o out.omnirefl.hpp -c source.cpp -- <compiler command...>
# If your system writes core files into the working directory:
gdb --batch -ex "thread apply all bt full" "$binary" ./core > omnirefl.bt.txt
# If your system uses systemd-coredump:
coredumpctl --output=omnirefl.core dump "$(basename "$binary")"
gdb --batch -ex "thread apply all bt full" \
"$binary" omnirefl.core > omnirefl.bt.txtIf no core file is produced, check cat /proc/sys/kernel/core_pattern; some
systems route core dumps to a crash service instead of the current directory.
Omnirefl is available under the MIT License.