Skip to content

Deserialization Reference

Header: rbe/dsrl.hpp (rbe/dsrl/deserialize.hpp, strategy tags in rbe/dsrl/tags.hpp, the lazy view in rbe/dsrl/proxy.hpp, rbe/dsrl/return_type.hpp)

Deserialization has no single right strategy: what a value costs to read depends on how much of it you need. rbe::deserialize<T> takes the strategy as a tag, and there is no default.

T                   value = rbe::deserialize<T>(buffer, rbe::dsrl::eager);          // decode everything now
rbe::dsrl::proxy<T> view  = rbe::deserialize<T>(buffer, rbe::dsrl::lazy);           // decode on access
T const&            ref   = rbe::deserialize<T>(buffer, rbe::dsrl::in_place);       // reinterpret the bytes
T&                  mut   = rbe::deserialize<T>(buffer, rbe::dsrl::in_place_mut);   // ...and write through them

buffer is a std::span<std::byte const>, or a std::span<std::byte> for in_place_mut. Every form has a narrow contract: the buffer must hold the whole value (and be aligned, for the in-place strategies), or the behavior is undefined. try_deserialize checks what it can instead.

Strategies

Strategy Tag T must be Returns Buffer copied Byte order converted
Eager rbe::dsrl::eager wirable; a non-trivial class also default constructible T yes, the whole value, now yes, per field
Lazy rbe::dsrl::lazy wirable rbe::dsrl::proxy<T> no, decoded on access yes, per field, on access
In place rbe::dsrl::in_place trivially_wirable T const& no, the buffer is the object no
In place, mutable rbe::dsrl::in_place_mut trivially_wirable T& no no

For a trivially wirable type the four strategies meet in the same memcpy or reinterpretation; the differences matter for types that need a conversion.

Eager

Order order = rbe::deserialize<Order>(buffer, rbe::dsrl::eager);

Every member is read and converted now; the result owns its data. Use it when you will touch every field, or when the buffer will be reused or freed before you are done.

Lazy

rbe::dsrl::proxy<Order> view = rbe::deserialize<Order>(buffer, rbe::dsrl::lazy);

auto price  = view.field<"price">();      // decoded now, by field name (compile-time checked)
auto second = view.field<1>();            // decoded now, by declaration index

Returns a proxy that borrows the buffer and decodes a member when asked for it. The buffer must outlive the view. Use it to route or filter on a few fields of a large message.

In place

struct Tick { std::uint64_t timestamp; std::uint32_t price; };   // trivially wirable

alignas(Tick) std::array<std::byte, sizeof(Tick)> aligned {};
Tick const& tick = rbe::deserialize<Tick>(aligned, rbe::dsrl::in_place);

The bytes are reinterpreted as T with std::start_lifetime_as: no copy, no conversion, which is why the type must be trivially wirable. The buffer must be aligned for T. in_place_mut does the same over a mutable span and returns T&, so writes go to the buffer.

Wider contract

Narrow and wider contracts

A function has a narrow contract when some of the inputs it accepts have undefined behavior: a partial function, in the mathematical sense. rbe::deserialize is one: a buffer that is too short compiles and runs, and the result is undefined.

The try_ forms have a wider contract, not a wide one. They check the precondition that can be checked, the buffer's size (and its alignment, in place), and report std::nullopt instead of faulting. What cannot be checked stays assumed: that the span points to live memory, and that the bytes are what the type says. The subset of inputs without undefined behavior is larger; it is not everything.

rbe::try_deserialize<T>(buffer, strategy) checks the checkable preconditions and returns std::optional<rbe::dsrl::return_type<S, T>>:

std::optional<T>                   value = rbe::try_deserialize<T>(buffer, rbe::dsrl::eager);
std::optional<rbe::dsrl::proxy<T>> view  = rbe::try_deserialize<T>(buffer, rbe::dsrl::lazy);
std::optional<T const&>            ref   = rbe::try_deserialize<T>(buffer, rbe::dsrl::in_place);
std::optional<T&>                  mut   = rbe::try_deserialize<T>(buffer, rbe::dsrl::in_place_mut);
auto view = rbe::try_deserialize<Order>(buffer, rbe::dsrl::lazy);          // checks buffer.size() >= wire_size_of<Order>()
auto cut  = rbe::try_deserialize<Order>(buffer.first(3), rbe::dsrl::lazy); // std::nullopt

if (view) {
  auto price = view->field<"price">();
  static_cast<void>(price);
}
Check Strategies
buffer.size() >= rbe::wire_size_of<T>() all
buffer.data() aligned to alignof(T) in_place, in_place_mut

in_place_mut takes a std::span<std::byte> and returns std::optional<T&>; the other three take a read-only span.

What it does not check, and cannot: that the memory is live for as long as a view or reference is used, and that the bytes are a T at all. Those remain the caller's, so the contract is still narrow, only wider.

Proxy

rbe::dsrl::proxy<T> is the lazy view: a std::span<std::byte const> plus T's wire layout, computed at compile time. It requires wirable<T> or explicitly_empty<T>.

// construction
rbe::dsrl::proxy<Order>                view    = rbe::deserialize<Order>(buffer, rbe::dsrl::lazy);
std::optional<rbe::dsrl::proxy<Order>> checked = rbe::dsrl::proxy<Order>::make(buffer);   // nullopt if too short
rbe::dsrl::proxy<Order>                direct {buffer};                                     // precondition: it fits

// fields
std::uint32_t price  = view.field<"price">();          // a primitive member: decoded, by value
auto          header = view.field<"header">();         // a class member: another proxy (its type carries the
std::uint8_t  type   = header.field<"type">();         //   inherited annotations, so use auto)
std::uint8_t  same   = view.field<"header", "type">(); // the same, as a path
std::uint8_t  by_ann = view.field<rbe::id>();          // the member carrying an annotation, searched in depth
auto          tag    = view.field<"tag">().value();    // a std::array member is a proxy too: value() decodes it
auto          second = view.field<1>();                // by declaration index

// the whole value
Order order = view.value();
Order again = *view;

// extents
std::size_t                size   = view.size();       // rbe::wire_size_of<Order>(), what the value occupies
std::span<std::byte const> bytes  = view.as_span();    // exactly those bytes
std::span<std::byte const> handed = view.buffer();     // the buffer the view was built over, possibly longer
std::byte const*           data   = view.data();

// comparison, when Order is equality comparable
bool equal = view == order and view == direct;
Member Description
value_type, size_type, buffer_type T, std::size_t, std::span<std::byte const>.
static make(buffer) -> std::optional<proxy> The checked constructor: nullopt if the buffer is shorter than wire_size_of<T>().
explicit proxy(buffer) Narrow: the buffer must hold the value.
field<"name">() The member by name. Requires wirable_class<T>; a name that is not a member is a compile error.
field<"a", "b", ...>() A path through nested members: field<"a">().field<"b">().
field<I>() The member at declaration index I.
field<Annotation>() The member carrying Annotation (rbe::id, rbe::frame_length, ...), searched into nested members. Requires contains_annotation<T, Annotation>.
value(), operator*() The whole T, decoded.
size(), static fixed_size() rbe::wire_size_of<T>().
as_span() Exactly the size() bytes the value occupies.
buffer(), data() The buffer the view was built over, which may be longer than the value, and its first byte.
operator==(proxy, T const&), operator==(proxy, proxy) Compare decoded values. Requires std::equality_comparable<T>.

What field<...>() returns depends on the member:

  • an integer or enumeration: the decoded value, by value;
  • a struct, std::array included: another proxy over the member's bytes. Its type carries the annotations the member inherits, so it is not spelled proxy<Member>: write auto, and call value() to decode it.

The view is a span: copy it freely, and keep the buffer alive for as long as any copy is read.

Strategy tags

The four tag types rbe::dsrl::eager_t, lazy_t, in_place_t and in_place_mut_t, and their instances eager, lazy, in_place and in_place_mut, satisfy the rbe::dsrl::strategy concept. Generic code constrains on the concept and asks rbe::dsrl::return_type<S, T> what a strategy hands back:

static_assert(rbe::dsrl::strategy<rbe::dsrl::eager_t>);
static_assert(rbe::dsrl::strategy<rbe::dsrl::lazy_t>);
static_assert(rbe::dsrl::strategy<rbe::dsrl::in_place_t>);
static_assert(rbe::dsrl::strategy<rbe::dsrl::in_place_mut_t>);

// what deserialize<T>(buffer, strategy) returns, for generic code
using result = rbe::dsrl::return_type<S, Order>;
S return_type<S, T>
eager_t T
lazy_t rbe::dsrl::proxy<T>
in_place_t T const&
in_place_mut_t T&

Frames

rbe::deserialize<F>(buffer) and rbe::try_deserialize<F>(buffer) for a frame F take no strategy: a frame is always read through its view. See Frames.