Binary Data and Hardware Boundaries
Pointers, References and Memory Layout

3.6 Binary Data and Hardware Boundaries

Hardware-informed C++ is not permission to reinterpret arbitrary memory as another type. Strict aliasing, alignment, object lifetime, padding, and endianness all matter when reading files, packets, device registers, or shared binary data.

Use std::byte for uninterpreted storage, std::bit_cast for safe same-size bit representations, and explicit serialization/deserialization for externally defined formats. std::endian describes the host's byte order; wire formats and files often use a fixed endianness that may differ.

Endianness and byte order

Multi-byte integers are stored as consecutive bytes. Little-endian layouts store the least significant byte at the lowest address; big-endian stores the most significant byte first. Network protocols and many file formats specify endianness explicitly, never assume the host matches.

#include <bit>
#include <cstdint>
#include <iostream>

int main() {
    if constexpr (std::endian::native == std::endian::little) {
        std::cout << "Host is little-endian\n";
    } else {
        std::cout << "Host is big-endian\n";
    }

    const std::uint16_t wireValue{0x1234};
    const std::uint16_t hostValue{std::byteswap(wireValue)}; // swap if wire format differs
    std::cout << std::hex << hostValue << '\n';
    return 0;
}
Inspect host endianness and swap a 16-bit value.

Strict aliasing: do not punning through unrelated pointers

The compiler may assume that writes through a float* do not change an unrelated int object. Reinterpreting the same address as a different type through an invalid pointer cast is undefined behavior under strict aliasing rules.

#include <bit>
#include <cstdint>

float unsafeReadAsFloat(std::uint32_t bits) {
    // BAD: type punning through an invalid pointer relationship
    return *reinterpret_cast<float*>(&bits);
}

float safeReadAsFloat(std::uint32_t bits) {
    return std::bit_cast<float>(bits); // OK: same size, no aliasing violation
}
Unsafe punning vs std::bit_cast.

Parsing a fixed-layout binary record

Treat external bytes as untrusted input. Check the span length, read fields explicitly, and convert endianness when the format requires it. Capstone 10.4 extends this pattern to a full telemetry stream.

#include <bit>
#include <cstddef>
#include <cstdint>
#include <span>

struct Record {
    std::uint32_t id;
    std::int16_t value;
};

bool parseRecord(std::span<const std::byte> bytes, Record& out) {
    if (bytes.size() < 6) return false;

    const auto* data = reinterpret_cast<const std::uint8_t*>(bytes.data());
    out.id = static_cast<std::uint32_t>(data[0])
           | (static_cast<std::uint32_t>(data[1]) << 8)
           | (static_cast<std::uint32_t>(data[2]) << 16)
           | (static_cast<std::uint32_t>(data[3]) << 24);
    out.value = static_cast<std::int16_t>(
        static_cast<std::uint16_t>(data[4])
      | (static_cast<std::uint16_t>(data[5]) << 8));
    return true;
}
Parse a 6-byte little-endian record from a byte span.

volatile is not thread synchronization

volatile is primarily for accesses whose observable side effects must not be optimized away, such as memory-mapped I/O on a platform that specifies it. It does not make an ordinary variable atomic, establish a happens-before relationship, or replace a mutex.

#include <bit>
#include <cstdint>

std::uint32_t bitsOf(float value) {
    static_assert(sizeof(float) == sizeof(std::uint32_t));
    return std::bit_cast<std::uint32_t>(value);
}

int main() {
    return bitsOf(1.0f) != 0 ? 0 : 1;
}
Inspect float bits without type-punning through an invalid pointer.

For actual device registers, follow the architecture and board support package documentation. Isolate platform-specific code behind a small tested interface so the rest of the program remains ordinary portable C++.

Chapter 3 Knowledge CheckQuestion 1 of 6

What happens to the stack memory address value when you add 1 to an integer pointer (int* p)?

Finished reading this lesson?