Quant developer HFT course · Module 2: Safe C++
Casts, aliasing and reading bytes safely
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.
| Cast | What it does | When to use it |
|---|---|---|
static_cast | Conversions the compiler can check: numeric conversions, up and down a class hierarchy without a run-time check, void* back to its real type | Most of the time |
dynamic_cast | Down or across a polymorphic hierarchy with a run-time check; returns null (pointers) or throws (references) if wrong | Rarely, off the hot path |
const_cast | Adds or removes const | Calling an old API that is not const-correct; writing to an object that was defined const is undefined |
reinterpret_cast | Reinterprets the bits of a pointer as another pointer type, or a pointer as an integer | Low-level code, with care: see below |
std::bit_cast (C++20) | Copies the bits of one trivially copyable value into another of the same size | Reading 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:
- Aliasing and lifetime. There is no
Headerobject at that address; there is an array ofchar. Reading it through aHeader*is undefined, even though the bytes are right. - Alignment. If
Headercontains auint64_tandbuf + offsetis 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. - 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?
static_cast converts the value (3.7 becomes 3), not the bits. The reinterpret_cast read violates strict aliasing. bit_cast and memcpy copy the representation with defined behaviour.
Question 2 of 3
Why is memcpy into a local struct usually as fast as casting the buffer pointer?
For a known small size, the compiler inlines the copy as plain loads. The generated code is typically identical, and the memcpy version is defined behaviour.
Question 3 of 3
Which types may legally be used to read the bytes of any object?
The aliasing exception covers the character types and std::byte. uint8_t is almost always unsigned char, but the standard does not require it.
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.