Skip to content

Annotate a type

Describe how a struct travels on the wire: byte order, packing, and how those choices reach nested types. The struct itself is untouched; only the bytes RBE reads and writes change.

Source: example/howto/annotations.cpp, CMake target howto_annotations.

Annotate the struct

An annotation on the struct applies to every member.

// Every member travels packed and big-endian
struct [[=rbe::pack, =rbe::big]] Header {
  std::uint16_t length;
  std::uint8_t  type;
  std::uint32_t sequence;
};

static_assert(sizeof(Header) == 8);                  // the struct keeps its C++ layout...
static_assert(rbe::wire_size_of<Header>() == 7);     // ...only the bytes on the wire are packed

Override on a member

A member's own annotation replaces the one it inherits, one dimension at a time: crc below is still packed, only its byte order changes.

// A member's own annotation wins over the one it inherits from the struct
struct [[=rbe::pack, =rbe::big]] Record {
  std::uint32_t                  timestamp;  // big-endian, inherited
  [[=rbe::little]] std::uint32_t crc;        // little-endian, explicit
};

Nested types

A nested struct with no annotations of its own inherits the enclosing struct's. One that has its own keeps them, so a type declared once with its wire format is written the same way wherever it appears.

struct Point {                                // no annotations of its own...
  std::int16_t x;
  std::int16_t y;
};

struct [[=rbe::pack_be]] Shape {
  Point         origin;                       // ...so it inherits pack and big from Shape
  std::uint8_t  sides;
};

struct [[=rbe::little]] LegacyFooter {        // a nested type with its own annotations keeps them
  std::uint32_t checksum;
};

struct [[=rbe::pack_be]] Message {
  Shape        shape;                         // packed, big-endian
  LegacyFooter footer;                        // packed (inherited), little-endian (its own)
};

static_assert(rbe::wire_size_of<Shape>() == 5);
static_assert(rbe::wire_size_of<Message>() == 9);

Presets

rbe::pack_le and rbe::pack_be cover the common pairs. rbe::derive<...> groups any annotations into a value you can name and reuse.

struct [[=rbe::pack_le]] A { std::uint32_t v; };            // same as [[=rbe::pack, =rbe::little]]
struct [[=rbe::pack_be]] B { std::uint32_t v; };            // same as [[=rbe::pack, =rbe::big]]

// your own preset: group annotations once, reuse them by name
inline constexpr auto wire_format = rbe::derive<rbe::pack, rbe::big, rbe::fmt>;

struct [[=wire_format]] C { std::uint32_t v; };

Defaults

With no annotation a type travels little-endian with its C++ layout, padding included. That makes it trivially wirable: the wire bytes are the object's bytes, and every operation is a memcpy.

// No annotation at all: little-endian and the regular C++ layout, padding included
struct Plain {
  std::uint8_t  flag;
  std::uint32_t value;
};

static_assert(rbe::wire_size_of<Plain>() == sizeof(Plain));  // 8, not 5
static_assert(rbe::trivially_wirable<Plain>);                 // so it is copied with one memcpy

Compile-time checks

Two annotations of the same kind in the same place, or an annotation on something it cannot apply to, is a compile error at the point of use. rbe::well_annotated is the check as a concept.

// Two annotations of the same dimension in one place is a mistake the compiler reports
struct [[=rbe::little, =rbe::big]] Conflicting { std::uint32_t v; };

static_assert(rbe::well_annotated<Message>);
static_assert(not rbe::well_annotated<Conflicting>);

See also: Annotations reference for every annotation and the exact inheritance rules.