On occasion, it is very useful to reinterpret raw bytes structured data - ints, floats, etc. Eamples include reading from a mmapped file, reading some sort of in-memory data frame, or other tasks that involve interpreting raw bytes.
Let's make the following assumptions:
- Both the input bytes and the computer are little-endian,
- All of the reads are properly aligned
- The code should be (somewhat) portable: it should work on the major compilers (gcc, clang, MSVC), and it should work on
x86-64andARM - ALl supported platforms are sane: bytes are 8 bits in size,
CHAR_BIT == 8, fixed-width integer typesint8_t,int16_t,int32_t, etc exist. - The source is constant (eg, we do not care if reads are re-ordered or optimized away)
- The code will be compiled with a modern optimizing compiler (eg, gcc or clang)
What are the best practices for interpreting raw bytes as primitive data types?
Scenario 1: loading a single value
Consider the following function:
// Load int32_t from src[0] to src[3]
inline int32_t load_int32( char const* src );
It's marked inline because we want it to be inlined so that the compiler can optimize it.
What's the preferred way to implement this function in C++17?
Option 1: use memcpy
- Pros: not UB, major compilers optimize this well because they know what's happening
- Cons: Debug build performance on MSVC sucks
inline int32_t load_int32( char const* src ) {
int32_t dest;
memcpy( &dest, src, sizeof( int32_t ) );
return dest;
}
Option 2: reinterpret_cast
- Pros: code is slightly cleaner, it's obvious what's going on
- Cons: I believe this is technically UB since
srcdoesn't originally point to aint32_t
inline int32_t load_int32( char const* src ) {
return *reinterpret_cast<int32_t const*>( src );
}
Scenario 2: loading an array of values
Prior to C++23, (afaik) there is no standards-compliant way to get a pointer-to-int. So the options are:
Option 1: write an iterator which uses memcpy under the hood
inline raw_byte_iter<int32_t> load_int32_array( char const* src ) {
return { src };
}
raw_byte_iter<T> holds a char const* and uses memcpy to load and store values:
template<class T>
class raw_byte_iter
{
char const* src;
public:
using reference = T;
using difference_type = ptrdiff_t;
// ...
raw_byte_iter( char const* src ) noexcept: src(src) {}
void operator++() { src += sizeof(T); }
raw_byte_iter operator+(ptrdiff_t diff) { return src + sizeof(T) * diff; }
T operator*() const noexcept {
T value;
memcpy( &value, src, sizeof(T) );
return value;
}
T operator[](ptrdiff_t i) const noexcept { return *( *this + i ); }
// ...
};
- Pros: This should optimize well on gcc and clang, msvc will hopefully be fine with it
- Cons:
- If you want this to be a proper random access iterator then the reference type has to be a value of T. Which is disgusting. It also involves writing a whole iterator class.
- You can't pass
raw_byte_iterto any low-level APIs which expect a pointer
Option 2: Use reinterpret_cast?
In this case, reinterpret_cast allows for a much cleaner implementation, but again, it's technically undefined behavior!
inline int32_t const* load_int32_array( char const* src ) {
return reinterpret_cast<int32_t const*>( src );
}
- Pros: very simple implementation, no need to worry about writing an iterator wrapper
- Pros: can be directly passed to other low-level APIs
- Cons: technically UB
Option 3 (C++23 only): std::start_lifetime_as_array
I believe C++23 allows us to bless this code with std::start_lifetime_as_array:
inline int32_t const* load_int32_array( char const* src, size_t n ) {
return std::start_lifetime_as_array( src, n );
}
- Pros: I believe this is the best solution
- Cons: only available in C++23 and above, I can't use it right now
Summary
The root of the question is this:
Does it matter that reinterpret_cast here is UB, or is this a scenario where compilers do the right thing because too much code breaks otherwise? What makes the most sense for a production codebase?