Omnirefl
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.
Sneak Peek
The complete runnable example implements the visitor and shows how to write reflected fields. Its setup and model are below:
// One .cpp; no declaration headers, metadata files, or reflection macros.
// Example visitor that prints reflected records during a depth-first walk
// of type dependencies. The struct keeps this logic below; an inline
// lambda would also work.
struct print_reachable_records {
template <omni::record_meta Root>
void operator()(Root) const;
};
template <typename T>
void print_reflection() {
// `reflected_call` makes metadata for `T` and its type dependencies available
// inside the visitor's `operator()` and the functions it calls.
omni::reflected_call(print_reachable_records{}, omni::type<T>);
}
// Mutating example; its implementation showcases several reflected arguments
// in one `reflected_call`.
template <typename... Values>
void update_fields(Values *...values);
// Omnirefl reflects globally accessible, named records (structs, classes, or
// unions) and enums on demand.
namespace oceanic {
/// Declaration comments are available as reflection metadata.
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.
using degrees = double;
degrees latitude = 0;
degrees longitude = 0;
};
position location;
// Reflected field properties are queryable.
mutable std::string name = "before"; //< `.is_mutable()` is true.
// Public non-static methods are reflected. Their parameter and return types
// join dependency discovery. Overloaded methods are skipped with a warning.
// 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(position other) const {
return location.latitude == other.latitude
&& location.longitude == other.longitude;
}
};
/** Measure a distance.
* @param scale Multiplier.
* @return The scaled distance.
*/
double distance(vessel, double scale) {
return scale;
}
// Primary templates are supported; names appear without template arguments.
template <typename T>
struct fleet { //< Discovered as an `omni::type_t` argument to `reflected_call`.
// Unsupported nested records emit a warning if discovered as dependencies.
struct not_supported {};
// Free functions are reflected only when discovered through a record field
// using the special `function_t` tag.
omni::function_t<distance> measure;
// Standard-library types are not reflected, but their dependency protocols
// still apply. The specialization for `T` is discovered via `mapped_type`.
std::map<std::string, T> vessels;
};
struct telemetry { //< Discovered as a value argument to `reflected_call`.
// Writing bit-fields without UB is supported via `.set_value()`.
mutable unsigned depth : 10 = 42;
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
Minimal CMake setup:
cmake_minimum_required(VERSION 3.20 FATAL_ERROR)
project(sneak_peek LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
find_package(omnirefl CONFIG REQUIRED)
add_executable(sneak_peek main.cpp)
if(MINGW AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
# MinGW's std::print terminal support lives in this library.
target_link_libraries(sneak_peek PRIVATE stdc++exp)
endif()
# 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(sneak_peek)
Build the example:
# Builds rerun instrumentation automatically for changed inputs.
# To trigger it manually:
# cmake --build build --target sneak_peek.omni
cmake --build build --target sneak_peek
Run it:
$ ./build/sneak_peek
// Primary templates are supported; names appear without template arguments.
oceanic::fleet {
measure(vessel, double scale [Multiplier.]) -> double; // Measure a distance.
// Standard-library types are not reflected, but their dependency protocols
// still apply. The specialization for `T` is discovered via `mapped_type`.
vessels: map<std::string, T>;
}
// Declaration comments are available as reflection metadata.
oceanic::vessel {
location: vessel::position (resolves to vessel::position);
// `.is_mutable()` is true.
name: string = before;
is_near(vessel::position other [Other position.]) -> bool; // Check proximity.
}
// Nested structs are supported recursively inside non-template records.
oceanic::vessel::position {
latitude: vessel::position::degrees = 0;
longitude: vessel::position::degrees = 0;
}
before: depth=42 sensor=108 name=before
after: depth=815 sensor=108 name=oceanic
This example is included in the installed package; see Examples, Tests and Benchmarks.
The comprehensive guide covers the remaining interface and compatibility features.
Seamless Experience
- 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.
Status
Omnirefl is under active testing and interface polishing. Version 0.1.0 is planned after the initial set of extensions is complete. Until then, interfaces and package layout may change without compatibility guarantees. Release notes aim to call out every breaking interface change.
Supported Scope
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
Dependency Protocols
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.
Is It Slow?
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.
Install
- 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.
Examples, Tests and Benchmarks
share/omnirefl packages tests, examples, and benchmarks. You may configure
the whole directory out of source, or any share/omnirefl/examples/<folder>
as a standalone project.
Examples, tests, and benchmarks are enabled by default; disable a category with
-DBUILD_EXAMPLES=OFF, -DBUILD_TESTING=OFF, or -DBUILD_BENCHMARKS=OFF.
Unsupported examples are skipped with a status message.
Alternatively, download and unpack the Cosmopolitan archive from the
latest release
into ./omnirefl:
mkdir -p omnirefl
curl -fL "$(curl -fsSL \
https://api.github.com/repos/sergio-eld/omnirefl/releases/latest \
| sed -n 's/.*"browser_download_url": "\(.*cosmo-universal.*\.tar\.gz\)".*/\1/p')" \
| tar -xz -C omnirefl --strip-components=1
# Use /usr when installed system-wide from a Debian package.
prefix="$PWD/omnirefl"
mkdir build && cd build
cmake "$prefix/share/omnirefl" -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo \
"-DCMAKE_PREFIX_PATH=$prefix"
cmake --build .
ctest --timeout 600 --output-on-failure --no-tests=error
Each share/omnirefl/examples/<folder> directory is self-contained. Configure one
independently, such as share/omnirefl/examples/sneak_peek, or copy it into
your own workspace. The Sneak Peek requires CMake 3.20 and a configured
C++23 toolchain.
Extension examples, tests, and benchmarks are grouped under
share/omnirefl/extensions:
serialization: RapidYAML-backed JSON/YAML serialization (C++11 compatible). See the standalone example.
Limitations
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.
How It Works
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 omni
This 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.