A C++ reflection tool built for a seamless experience without macros* or UB.
* Some compatibility QoL before C++17/20 uses macros.
Obligatory self-reflection meta joke.
This example intentionally uses C++23 syntax and library facilities to reduce
verbosity. The demonstrated reflection features remain available through
Omnirefl's C++11-compatible interfaces.
Include directives and main() are omitted to keep the block compact; see the
complete source.
// One .cpp; no declaration headers, metadata files, or reflection macros.
namespace oceanic {
/// `.documentation()` returns this text.
struct vessel { //< Discovered as the `mapped_type` of `fleet<T>::vessels`.
// Nested structs are supported recursively inside non-template records.
struct position { //< Discovered via the `location` field.
double latitude = 0;
double longitude = 0;
};
using coordinates = position;
// Aliases are preserved for field types. For the `location` field,
// `.spelled_type_name()` returns the `vessel::coordinates` type name;
// `.type_name()` on its reflected type returns `vessel::position` without
// the namespace.
coordinates location;
// Reflected field properties are queryable.
mutable std::string name = "before"; //< `.is_mutable()` is true.
// Public non-static methods are reflected. Overloaded methods are skipped
// with a warning. `.documentation()` exposes the comment; Doxygen parameter
// descriptions, such as `@param` or `\param[in]`, are available through
// parameter metadata.
/** Check proximity.
* @param other Other position.
* @return Whether the positions are near.
*/
bool is_near(coordinates other) const {
return location.latitude == other.latitude
&& location.longitude == other.longitude;
}
};
// A `function_t` record field exposes this free function to reflection.
// Free functions cannot be standalone reflection roots; see "How It Works".
/** Measure a distance.
* @param scale Distance scale.
* @return The scaled distance.
*/
double distance(vessel, double scale) {
return scale;
}
// Primary templates are supported; `.type_name()` returns `"fleet"` without
// template arguments.
template <typename T>
struct fleet { //< Discovered as an `omni::type_t` argument to `reflected_call`.
// Records nested inside template records are not supported.
// struct not_supported {};
// Its signature types join dependency discovery.
omni::function_t<distance> measure;
// `std` types are not reflected, but dependency protocols apply.
// Here `mapped_type` discovers `T`.
std::map<std::string, T> vessels;
};
struct telemetry { //< Discovered as a value argument to `reflected_call`.
mutable unsigned depth : 10 = 42; //< `.set_value()` writes bit-fields.
const unsigned sensor = 108; //< `.is_const()` is true.
// Only public fields are reflected.
private:
[[maybe_unused]] unsigned john_cena = 49; //< can't see
};
} // namespace oceanic
// Templates may be declared outside the reflected scope and use reflection
// when called from it, but must not be instantiated outside that scope.
template <omni::record_meta RecordMeta>
std::string describe_record(RecordMeta record) {
namespace fn = omni::fn; //< Functional QoL for tuple-like values.
const auto describe_function = //
[](omni::function_meta auto function) {
const auto parameters = function.parameters()
| fn::map([](omni::function_param_meta auto parameter) {
const auto description = std::format("{}{}",
parameter.spelled_type_name(),
std::string_view{parameter.name()}.empty()
? std::string{}
: std::format(" {}", parameter.name()));
return std::string_view{parameter.documentation()}.empty()
? description
: std::format("{} [{}]",
description,
parameter.documentation());
})
| fn::foldl(
[](std::string before, const std::string ¶meter) {
return before.empty() ? parameter : before + ", " + parameter;
},
std::string{});
return std::format(" {}({}) -> {}; // {}\n",
function.name(),
parameters,
function.returns().spelled_type_name(),
function.documentation());
};
const auto fields = record.public_fields()
| fn::map([describe_function]<typename Member>(Member member) {
if constexpr (omni::function_meta<Member>)
return describe_function(member);
else {
static_assert(omni::field_meta<Member>);
const auto field_description = std::format(" {}: {}", member.name(),
// Use `.spelled_qualified_type_name()` to preserve namespaces.
member.spelled_type_name());
const std::optional default_value = std::invoke(
[] -> std::optional<std::string> {
// can: int value = 8 * 100 + 15;
// can't: int value = make_value();
if constexpr (Member::has_default_value_access()
&& std::formattable<typename Member::type, char>)
return std::optional{
std::format(" = {}", Member::default_value())};
return std::nullopt;
});
// Fundamental and standard-library types are not reflected, so the
// metadata query for the actual type name is unavailable for them.
if constexpr (omni::is_reflected<typename Member::type>::value)
return std::format("{} (resolves to {}){};\n",
field_description,
omni::meta_for<typename Member::type>::type_name(),
default_value.value_or(""));
return std::format("{}{};\n",
field_description,
default_value.value_or(""));
}
})
| fn::foldl(std::plus{}, std::string{});
const auto methods = record.public_methods()
| fn::map(describe_function)
| fn::foldl(std::plus{}, std::string{});
return std::format("{} {{\n{}{}}}",
record.qualified_type_name(),
fields,
methods);
}
/*
`main()` uses `describe_record()` to print discovered records in depth-first order:
```
oceanic::fleet {
measure(vessel, double scale [Distance scale.]) -> double; // Measure a distance.
vessels: map<std::string, T>;
}
oceanic::vessel {
location: vessel::coordinates (resolves to vessel::position);
name: string = before;
is_near(vessel::coordinates other [Other position.]) -> bool; // Check proximity.
}
oceanic::vessel::position {
latitude: double = 0;
longitude: double = 0;
}
```
*/
void print_field_updates(oceanic::telemetry telemetry,
const oceanic::vessel vessel) {
namespace fn = omni::fn; //< Functional QoL for tuple-like values.
using namespace std::string_view_literals;
std::println("before: depth={} sensor={} name={}",
static_cast<unsigned>(telemetry.depth), telemetry.sensor, vessel.name);
const auto write_fields = [](auto &value) {
omni::reflected_call(
[](omni::record_binding auto record) //< Non-owning binding to `value`.
// Explicit result defers scope instantiation until reflection exists.
-> void {
// Reflected scope: metadata is available here and in called templates.
record.public_fields()
| fn::filter<[]<typename Member> {
using binding = std::remove_cvref_t<Member>;
if constexpr (omni::field_binding<binding>)
return binding::is_mutable();
return false; //< Special function field.
}>()
| fn::each([](omni::field_binding auto field) {
constexpr std::string_view name = field.name();
if constexpr ("depth"sv == name)
field.set_value(815u); //< Writable bit-field.
else if constexpr ("name"sv == name)
field.set_value("oceanic"); //< Mutable field of a const object.
});
},
value);
};
write_fields(telemetry);
write_fields(vessel);
std::println("after: depth={} sensor={} name={}",
static_cast<unsigned>(telemetry.depth), telemetry.sensor, vessel.name);
}
// For `print_field_updates({}, {})`:
// before: depth=42 sensor=108 name=before
// after: depth=815 sensor=108 name=oceanicMinimal 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 23)
# 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.omniThe 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 is under active testing and interface polishing while its first extensions are being developed. Until at least 0.1.0, interfaces and package layout may change without compatibility guarantees. Release notes aim to call out every breaking interface change.
<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 - default member initializer presence and best-effort access to values accepted for reproduction in generated metadata; skipped values emit a warning and remain distinguishable from fields without an initializer
- read access, moving through
std::move(field).value(), 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, static fields, and fields inherited through non-public bases are omitted
- Public function metadata:
- non-static public member functions without parameter packs; overloaded methods are skipped with a warning, while conversion operators and immediate functions are omitted
- fixed
function_tfields as special field members for free or static functions; dependent function-tag fields in primary templates remain ordinary fields without function metadata - source name, documentation, pointer, arity, parameter metadata, and return metadata; parameter and return metadata include source-spelled type names and tagged documentation
- 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- record and enum metadata expose their domain type through
reflected_type; the generated metadata template argument is intentionally opaque - field bindings expose the cv-qualified bound record type separately from opaque field metadata
- one callable can receive multiple value and type arguments
- value bindings preserve const/volatile and lvalue/rvalue qualification; callable value and reference returns are preserved
omni::reflected(...)andis_reflected<T>query generated dependency metadata from inside the callable
Additional reflected types are discovered through:
- public field types
- public bases and transitive public bases
- public fields of primary template records
- parameter and return types of supported function metadata
- 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 callable must be either a generic lambda or a type with a templatedoperator(). Its return type must not depend on instantiating the callable 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. default_value()is generated only when the initializer appears safe to copy into generated metadata. Detection is conservative and best effort. Function calls, declaration references,this, macros, and dependent expressions are skipped with a warning.has_default_member_initializer()still reports the declaration, whilehas_default_value_access()reports whether its value is available.- Incomplete dependency types are skipped with a warning. A class-template dependency is also skipped when instantiating it would require an incomplete type argument.
- 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.
reflected_callrejects them as roots; explicit or partial specialization dependencies are skipped with a warning. - 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> {
// Internal discovery hook for the reflected C++ type.
using type = 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> {
// Internal discovery hook for the reflected C++ type.
using type = T;
// 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, or records nested in template records. Constrained primary templates and explicit or partial specializations are also unsupported.
Functions have no equivalent general forward-declaration strategy: reproducing
a declaration requires its parameter types and overload identity, which may not
be nameable before the source declaration. Therefore function_t is not a
standalone reflection root. The first implementation discovers it only as a
special field member of a reflected record, where generated access can remain
dependent on the owning record. The function's parameter and return types then
enter dependency discovery on a best-effort basis.
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.