Skip to content
Work Practice Products Coding interviews Machine learning coding Quant developer HFT course Blog Method Why me About Enquire Book a call

Quant developer HFT course · Module 2: Safe C++

Casts, aliasing and reading bytes safely

Lesson · 11 min

The question

You receive a binary market-data message in a char buffer. Someone wrote reinterpret_cast<const Header*>(buf) to read the header. Is that correct?

The five casts

C++ splits the old C cast into named casts, so that each one says what kind of conversion it is.

CastWhat it doesWhen to use it
static_castConversions the compiler can check: numeric conversions, up and down a class hierarchy without a run-time check, void* back to its real typeMost of the time
dynamic_castDown or across a polymorphic hierarchy with a run-time check; returns null (pointers) or throws (references) if wrongRarely, off the hot path
const_castAdds or removes constCalling an old API that is not const-correct; writing to an object that was defined const is undefined
reinterpret_castReinterprets the bits of a pointer as another pointer type, or a pointer as an integerLow-level code, with care: see below
std::bit_cast (C++20)Copies the bits of one trivially copyable value into another of the same sizeReading a float's bits as an integer, and similar

A C-style cast (T)x tries these in turn and will happily do a reinterpret_cast or remove const without saying so. That is why code review flags them.

Strict aliasing

The compiler assumes that pointers to different types do not point to the same memory, with a few exceptions: char, unsigned char and std::byte may alias anything, and signed and unsigned versions of the same type may alias each other. This lets it keep values in registers instead of reloading them after every write through an unrelated pointer.

Reading an object through a pointer of an unrelated type breaks that assumption and is undefined behaviour:

float f = 1.0f;
std::uint32_t bits = *reinterpret_cast<std::uint32_t*>(&f);   // undefined: uint32_t does not alias float
std::uint32_t ok   = std::bit_cast<std::uint32_t>(f);         // defined: copies the bits

Most of the time the first line produces the expected answer, which is why it survives. Under optimisation it can produce stale values, because the compiler reorders the read past a write it believes is unrelated.

Reading a struct out of a buffer

Now the question. reinterpret_cast<const Header*>(buf) has three problems:

  1. Aliasing and lifetime. There is no Header object at that address; there is an array of char. Reading it through a Header* is undefined, even though the bytes are right.
  2. Alignment. If Header contains a uint64_t and buf + offset is not 8-byte aligned, the access is misaligned. x86 tolerates it with at most a small penalty; other architectures fault, and the compiler may emit instructions that assume alignment.
  3. Layout and byte order. The struct's padding and field order must match the wire format exactly, and the integers must be in the machine's byte order. Exchange protocols are often little-endian, sometimes big-endian.

The portable way is to copy the bytes into a real object:

#pragma pack(push, 1)
struct Header {                       // matches the exchange's wire layout, no padding
    std::uint16_t length;
    std::uint16_t type;
    std::uint32_t seq;
    std::uint64_t send_time_ns;
};
#pragma pack(pop)
static_assert(sizeof(Header) == 16);
static_assert(std::is_trivially_copyable_v<Header>);

Header read_header(const char* buf) {
    Header h;
    std::memcpy(&h, buf, sizeof h);   // defined behaviour, any alignment
    return h;                         // convert byte order here if the wire is big-endian
}

This looks slower and is not. Compilers recognise a fixed-size memcpy into a local and turn it into the same one or two load instructions the cast would have produced. C++23 adds std::start_lifetime_as for the case where you really want to treat the bytes in place as an object, but memcpy remains the clearest answer.

For many fields, a common pattern is accessor functions that memcpy each field from its offset in the buffer, which avoids packing pragmas altogether.

dynamic_cast on the hot path

dynamic_cast walks the type information of the object to check the cast, which costs far more than a virtual call and varies with the depth of the hierarchy. If hot code needs it, the design usually wants a virtual function, a type tag with a switch, or a std::variant instead.

Check yourself

Question 1 of 3

Which cast is the right way to read the bits of a double as a uint64_t?

Question 2 of 3

Why is memcpy into a local struct usually as fast as casting the buffer pointer?

Question 3 of 3

Which types may legally be used to read the bytes of any object?

In the interview

Say your own answer out loud first, then open the one we would give.

What is the difference between static_cast and reinterpret_cast?

static_cast performs conversions the compiler can check: numeric conversions, base and derived pointers, void* back to its real type. reinterpret_cast just treats a pointer's bits as a different pointer type, with no checks; reading through the result is only valid in narrow cases, so it belongs in low-level code with a comment.

What is strict aliasing?

The rule that you may not read an object through a pointer to an unrelated type, except through char, unsigned char or std::byte. Compilers rely on it to keep values in registers. Breaking it is undefined behaviour that tends to show up only under optimisation.

How do you parse a binary message from a byte buffer?

Define a trivially copyable struct matching the wire layout, checked with static_asserts on its size, then memcpy the bytes into a local of that type, and fix byte order if the protocol requires it. The compiler turns the memcpy into ordinary loads, so it costs nothing, and it avoids aliasing and alignment problems.