C++26 reflection utilities. Header-only. Zero dependencies. Zero boilerplate.
struct Trade {
int id; std::string symbol; double price;
};
Trade t{1, "MSFT", 420.69};
auto json = reflect::to_json(t); // {"id":1,"symbol":"MSFT","price":420.69}
auto t2 = reflect::from_json<Trade>(json);No macros. No code generation. Add a field, JSON updates automatically.
| Header | What it does | Inspired by |
|---|---|---|
reflect/enum.hpp |
Enum ↔ string, names, values, flags | magic_enum |
reflect/print.hpp |
Pretty-print any struct | Rust derive(Debug) |
reflect/compare.hpp |
Automatic ==, <=>, std::hash | Rust derive(PartialEq, Hash) |
reflect/tuple.hpp |
Struct ↔ tuple conversion | Boost.PFR |
reflect/json.hpp |
JSON serialize/deserialize | nlohmann/json, glaze |
reflect/visit.hpp |
for_each_field(obj, visitor) |
Boost.PFR |
reflect/args.hpp |
CLI parsing from a struct | Rust's clap |
reflect/diff.hpp |
Struct diff / change detection | — |
reflect/format.hpp |
std::format integration |
— |
reflect/type_name.hpp |
Clean demangled type names | Boost.TypeIndex |
Every header is independent. Include only what you need.
#include <reflect/enum.hpp>
#include <reflect/print.hpp>
#include <reflect/json.hpp>
enum class Color { red, green, blue };
struct Point { int x; int y; };
struct Line { Point start; Point end; std::string label; };
int main() {
// Enum reflection
reflect::enum_to_string(Color::red); // → "red"
reflect::enum_from_string<Color>("blue"); // → Color::blue
reflect::enum_count<Color>(); // → 3
// Pretty-print
Line line{{0,0}, {10,20}, "diagonal"};
reflect::to_string(line);
// → Line{.start=Point{.x=0, .y=0}, .end=Point{.x=10, .y=20}, .label="diagonal"}
// JSON (nested structs, vectors, optionals — all automatic)
reflect::to_json(line);
// → {"start":{"x":0,"y":0},"end":{"x":10,"y":20},"label":"diagonal"}
auto line2 = reflect::from_json<Line>(R"({"start":{"x":1,"y":2},"end":{"x":3,"y":4},"label":"test"})");
}Compile:
# Bloomberg Clang fork (recommended for now)
clang++ -std=c++26 -freflection-latest -Iinclude main.cpp
# GCC trunk / GCC 16+
g++ -std=c++26 -Iinclude main.cppDon't have a P2996-capable compiler installed? The repo ships a pinned toolchain. Requires only Docker:
git clone https://github.com/shoom1/reflect-cpp26.git
cd reflect-cpp26
./scripts/docker-build.sh # first run: builds toolchain image (~30–60 min) + project
./scripts/docker-test.sh # runs all tests
./scripts/docker-run.sh reflect_example_showcaseThe Dockerfile builds Bloomberg's clang-p2996 fork from source pinned
to a specific commit. Trust chain: Docker Desktop → ubuntu:24.04 →
bloomberg/clang-p2996. No third-party prebuilt images.
The first build takes 30–60 minutes (LLVM compile). Subsequent runs are
seconds — Docker caches the image, and cmake --build is incremental.
CI mirrors this setup — see .github/workflows/.
#include <reflect/enum.hpp>
enum class Status { pending, active, closed };
reflect::enum_to_string(Status::active); // → "active"
reflect::enum_from_string<Status>("closed"); // → std::optional<Status>{Status::closed}
reflect::enum_from_string<Status>("invalid"); // → std::nullopt
reflect::enum_count<Status>(); // → 3
reflect::enum_names<Status>(); // → std::array{"pending", "active", "closed"}
reflect::enum_values<Status>(); // → std::array{Status::pending, ...}
reflect::enum_entries<Status>(); // → std::array{pair{Status::pending, "pending"}, ...}
reflect::enum_contains<Status>(1); // → true
reflect::enum_cast<Status>(99); // → std::nullopt
reflect::enum_index(Status::active); // → 1
// Bitmask / flags
enum class Perm : unsigned { read = 1, write = 2, exec = 4 };
template <> struct reflect::enable_bitmask_operators<Perm> : std::true_type {};
auto p = Perm::read | Perm::exec; // works thanks to enable_bitmask_operators
reflect::flags_to_string(p); // → "read|exec"
reflect::flags_from_string<Perm>("read|write"); // → Perm::read | Perm::write#include <reflect/visit.hpp>
struct Config { std::string host; int port; bool tls; };
Config cfg{"localhost", 8080, true};
// Iterate fields
reflect::for_each_field(cfg, [](std::string_view name, auto const& value) {
std::cout << name << ": " << value << "\n";
});
// Field metadata
reflect::field_count<Config>(); // → 3
reflect::field_names<Config>(); // → {"host", "port", "tls"}
reflect::has_field<Config>("port"); // → true
// Mutate fields
reflect::for_each_field(cfg, [](std::string_view name, auto& value) {
if constexpr (std::is_same_v<std::remove_cvref_t<decltype(value)>, int>)
value *= 2;
});
// cfg.port is now 16160#include <reflect/json.hpp>
struct Order {
int id;
std::string symbol;
double price;
std::optional<std::string> note;
std::vector<int> fills;
};
Order order{1, "MSFT", 420.69, "limit", {100, 200, 50}};
// Serialize
reflect::to_json(order);
// → {"id":1,"symbol":"MSFT","price":420.69,"note":"limit","fills":[100,200,50]}
// Pretty-print
reflect::to_json(order, {.indent = 2});
// Skip nullopt fields
reflect::to_json(order, {.skip_nullopt = true});
// Deserialize
auto order2 = reflect::from_json<Order>(json_string);
// Unknown fields in JSON are silently ignored — forward-compatible#include <reflect/tuple.hpp>
struct Vec3 { float x, y, z; };
Vec3 v{1.0f, 2.0f, 3.0f};
auto t = reflect::to_tuple(v); // std::tuple<float,float,float>
auto v2 = reflect::from_tuple<Vec3>(t); // Vec3{1, 2, 3}
reflect::get<0>(v); // → 1.0f
reflect::apply(v, [](float x, float y, float z) { return x + y + z; }); // → 6.0f#include <reflect/args.hpp>
struct Args {
std::string input; // --input <STRING> (required)
int verbose = 0; // --verbose <INT> (required)
bool debug = false; // --debug (flag)
std::optional<std::string> output; // --output <STRING> (optional)
std::vector<std::string> files; // --files a.txt b.txt (multi-value)
};
int main(int argc, char** argv) {
auto args = reflect::parse_args<Args>(argc, argv);
// Automatically handles:
// --help / -h → prints generated help text
// --input foo.txt --verbose 3 --debug --files a.txt b.txt
// Missing required values → throws reflect::args_error
// Unknown flags → throws reflect::args_error
// Field name underscores → dashes: my_field → --my-field
}
// Generate help text:
reflect::args_help<Args>("myapp");
// Usage: myapp [OPTIONS]
//
// Options:
// --input <STRING> (required)
// --verbose <INT> (required)
// --debug (flag)
// --output <STRING> (optional)
// --files <STRING>... (multi-value)
// --help Show this help message#include <reflect/diff.hpp>
struct Config { std::string host; int port; bool tls; };
Config a{"localhost", 8080, false};
Config b{"localhost", 9090, true};
reflect::has_changes(a, b); // → true
reflect::change_count(a, b); // → 2
auto names = reflect::changed_field_names(a, b);
// → {"port", "tls"}
reflect::diff_summary(a, b);
// → "port: 8080 → 9090, tls: 0 → 1"
auto entries = reflect::diff(a, b);
// entries[0] = {.name="host", .index=0, .changed=false}
// entries[1] = {.name="port", .index=1, .changed=true}
// entries[2] = {.name="tls", .index=2, .changed=true}
// Selectively apply changes
reflect::apply_changes(a, b); // copy all changed fields
reflect::apply_changes(a, b, [](auto name, auto& old_v, auto& new_v) {
return name == "port"; // only copy port
});#include <reflect/format.hpp>
struct Point { int x; int y; };
REFLECT_MAKE_FORMATTABLE(Point);
// Now works with std::format and std::print:
std::format("{}", Point{3, 7}); // → "Point{.x=3, .y=7}"
std::format("{:j}", Point{3, 7}); // → {"x":3,"y":7}
std::format("{:J}", Point{3, 7}); // → pretty JSON
// Without the macro, use reflect::format() directly:
reflect::format(Point{3, 7}); // → "Point{.x=3, .y=7}"| Compiler | Version | Flag | Status |
|---|---|---|---|
| Bloomberg Clang fork | p2996 branch |
-freflection-latest |
✅ Tested |
| GCC | 16+ (trunk) | -std=c++26 |
Untested |
| EDG (Compiler Explorer) | — | — | Untested |
| Clang mainline | — | — | ❌ Not yet |
| MSVC | — | — | ❌ Not yet |
This library requires C++26 with P2996 reflection support. Try it without
installing anything: open Compiler Explorer, pick
"x86-64 clang (experimental P2996)" from the compiler dropdown, and
paste in any of the snippets above. Add -std=c++26 -freflection-latest
to the compiler flags box.
Header-only — just copy include/reflect/ into your project:
cp -r include/reflect/ /your/project/include/Or with CMake:
add_subdirectory(reflect)
target_link_libraries(your_target PRIVATE reflect)Or with CMake FetchContent:
include(FetchContent)
FetchContent_Declare(reflect
GIT_REPOSITORY https://github.com/shoom1/reflect-cpp26.git
GIT_TAG main
)
FetchContent_MakeAvailable(reflect)
target_link_libraries(your_target PRIVATE reflect)- Enum ↔ string
- Pretty-print
- Comparison & hashing
- Tuple conversion
- JSON serialization
- Field iteration / visitor
- Type names
- CLI argument parsing (struct → arg parser)
- Struct diff (
reflect::diff(a, b)) -
std::formatintegration - Binary serialization (MessagePack / CBOR)
- P3394 annotation support for field renaming, skip, validation
- CSV serialization
- Automatic
std::formatterwithout macro (once compilers support it) - Config file loading (TOML/YAML → struct)
MIT — do whatever you want.
PRs welcome. Please include tests and keep each header independent.