Skip to content

Core Reference

Header: rbe/core.hpp

The layer the others build on: the concepts that say what RBE can encode, the one layout query, byte order helpers, the debug formatter and a small overload set.

Type concepts

Headers: rbe/core/wirable_concepts.hpp, rbe/core/trivially_wirable_concepts.hpp, rbe/core/wirable_primitives.hpp

Concept Satisfied by
rbe::introspectable<T> A type whose members reflection can enumerate (std::meta::is_enumerable_type).
rbe::trivially_wirable_primitive<T> An integral type (bool and character types included) or an enumeration.
rbe::custom_wirable<T> A type with an rbe::custom<T> specialization that is not a primitive. Reserved for the planned customization point.
rbe::wirable_primitive<T> Either of the two above.
rbe::wirable<T> A primitive; an array (std::array or bounded C array) of primitives; or a class type with no base classes, at least one member, every member wirable, and every member public unless the class is annotated friendly.
rbe::wirable_class<T> wirable and a class type.
rbe::wirable_range<T> wirable and a std::ranges::range.
rbe::trivially_wirable<T> wirable, not custom_wirable, and a type whose wire layout equals its in-memory layout: a primitive, an array of them, or a trivially copyable standard-layout class whose members are all trivially wirable and whose annotations change nothing (no pack that removes padding, no byte swap on this host).
rbe::trivially_wirable_class<T>, rbe::trivially_wirable_range<T> trivially_wirable and a class type, or a contiguous range.
enum class kind : std::uint8_t { a, b };
struct Point { std::int16_t x, y; };
struct [[=rbe::pack_be]] Packed { std::uint8_t tag; std::uint32_t value; };
struct Base { std::uint8_t b; };
struct Derived : Base { std::uint8_t d; };

static_assert(rbe::wirable<std::uint32_t>);             // integers...
static_assert(rbe::wirable<kind>);                      // ...enumerations...
static_assert(rbe::wirable<std::array<kind, 4>>);       // ...arrays of integers or enumerations...
static_assert(rbe::wirable<Point>);                     // ...and classes of wirable members
static_assert(not rbe::wirable<double>);                // floating point is not supported yet
static_assert(not rbe::wirable<std::array<Point, 2>>);  // nor arrays of classes
static_assert(not rbe::wirable<std::vector<int>>);      // nor variable-size ranges
static_assert(not rbe::wirable<Derived>);               // nor base classes

class [[=rbe::friendly]] Account {                      // not an aggregate: friendly opens its private members to RBE
public:
  constexpr auto balance() const -> std::int32_t { return balance_; }
private:
  std::int32_t balance_ {};
};
static_assert(rbe::wirable<Account>);

static_assert(rbe::trivially_wirable<Point>);           // in-memory layout == wire layout: one memcpy
static_assert(not rbe::trivially_wirable<Packed>);      // packed and byte-swapped: member by member

static_assert(rbe::wirable_class<Point> and not rbe::wirable_class<kind>);
static_assert(rbe::wirable_range<std::array<kind, 4>>);
static_assert(rbe::trivially_wirable_primitive<kind>);

A wirable class does not have to be an aggregate, but its members have to be public: a class with a private member is not wirable until it is annotated =rbe::friendly, the author's consent to put its representation on the wire. Constructors and member functions do not matter. What a class is does limit the strategies: the eager one needs it default constructible, and the in-place ones need it trivially wirable, which asks for standard layout as well.

Not wirable today: floating point, arrays of classes, variable-size ranges (std::vector, std::string), std::optional, base classes, bit-fields. The feature matrix tracks them.

The predicates behind the concepts are consteval functions over std::meta::info, usable from your own reflection code: rbe::is_trivially_wirable_primitive(info), rbe::is_custom_wirable(info), rbe::is_wirable_primitive(info), rbe::is_trivially_wirable(info).

Wire size

Header: rbe/core/memory_layout.hpp

static_assert(sizeof(Packed) == 8);
static_assert(rbe::wire_size_of<Packed>() == 5);        // consteval: usable as an array bound
static_assert(rbe::wire_size_of<Point>() == 4);

rbe::wire_size_of<T>() is the number of bytes T occupies on the wire, honoring its annotations: with pack, the sum of its members' wire sizes; otherwise sizeof(T); zero for an empty type. It is consteval, so it sizes arrays and feeds static_asserts. It is the only layout query the library exposes.

Byte order

Header: rbe/core/endian.hpp, namespace rbe::endian

Symbol Description
order using order = std::endian; the type every byte-order annotation and helper takes.
to_native<From>(v) v, an integer in byte order From, converted to the host's order. Identity when From is order::native.
native_to<To>(v) The same conversion, named for the other direction.
load<T, O>(std::byte const* src) Read a T stored in byte order O at src.
store<T, O>(std::byte* dst, T v) Write v at dst in byte order O.
using rbe::endian::order;   // = std::endian

std::uint32_t host = 0x0102'0304;
std::uint32_t wire = rbe::endian::native_to<order::big>(host);   // byte-swapped on a little-endian host
std::uint32_t back = rbe::endian::to_native<order::big>(wire);   // and back

std::array<std::byte, 4> bytes {};
rbe::endian::store<std::uint32_t, order::big>(bytes.data(), host);       // 01 02 03 04
std::uint32_t loaded = rbe::endian::load<std::uint32_t, order::big>(bytes.data());

All four are constexpr and work on integral types.

Formatting

Header: rbe/core/fmt.hpp

A type annotated =rbe::fmt (or rbe::debug) gets a std::formatter specialization and an operator<<(std::ostream&, T const&). Both print the type name and every member as .name = value, one per line, recursing into nested members through their own formatters, base classes and bit-fields included.

struct [[=rbe::fmt]] Inner { std::uint8_t x; };
struct [[=rbe::debug]] Outer { Inner inner; std::uint16_t y; };   // rbe::debug is derive<fmt>

auto show(Outer const& value) -> std::string {
  return std::format("{}", value);   // "Outer {\n  .inner = Inner {\n    .x = 1,\n  },\n  .y = 2,\n}"
}

A nested struct needs a formatter of its own: =rbe::fmt on it, or a std::formatter specialization. A std::formatter<std::endian> is provided as well, printing little or big.

Custom types

Header: rbe/core/custom.hpp

Planned

rbe::custom<T> is meant to be the customization point for a type whose encoding is not the layout of its members. Today only the primary template is declared, and custom_wirable detects a specialization of it, but the path through it is not usable: a custom type gets sizeof(T) bytes on the wire with no way to state its own size, and only the eager strategies reach the specialization. The intended shape is two static functions, serialize(std::span<std::byte>, T const&) -> std::size_t and deserialize(std::span<std::byte const>) -> T. See the feature matrix.

Overload set

Header: rbe/core/overload_set.hpp

rbe::overload<Fs...> inherits the call operators of the callables it is built from, with a deduction guide, so a set of lambdas becomes one visitor. rbe::frames::dsrl::any::match builds one from its callbacks.

auto const visit = rbe::overload {
  [](std::uint32_t) { return 1; },
  [](std::string const&) { return 2; },
};