reinterpret_cast vs bit shifts for extracting 32-bit integers from bytes, with either endianness I've written some simple helper functions that read data types from a vector of bytes (binary files) and having a tough time deciding which route to go. 

Method 1 : reinterpret_cast

int32_t read_32s(const std::vector&lt;uint8_t&gt; &amp;buf, const unsigned offset,
                 const bool bswap = false) {
  // Check for out of bounds
  if (offset &gt; buf.size() - sizeof(int32_t)) {
    // error handling
  }
  // Cast the pointer to int32_t and get the first value
  const auto val = reinterpret_cast&lt;const int32_t *&gt;(&amp;buf[offset])[0];
  // Swap bytes if necessary
  if (bswap) {
    return (((val &amp; 0xFF000000) &gt;&gt; 24) | ((val &amp; 0x00FF0000) &gt;&gt; 8) |
            ((val &amp; 0x0000FF00) &lt;&lt; 8) | ((val &amp; 0x000000FF) &lt;&lt; 24));
  }
  return val;
}


The biggest advantage with this method is probably performance, readability is debatable. This basically compiles down to a single instruction, two if you need to swap the bytes. The problem I have with this is casting like this in modern C++ is frowned upon, and I am unsure how safe it actually is despite bounds checking first. How does alignment work?

Method 2 : classic bit shifts

int32_t read_32s(const std::vector&lt;uint8_t&gt; &amp;buf, const unsigned offset,
                 const bool bswap = false) {
  // Check for out of bounds
  if (offset &gt; buf.size() - sizeof(int32_t)) {
    // error handling
  }
  // Swap bytes if necessary
  if (bswap) {
    return (buf[offset] &lt;&lt; 24) | (buf[offset + 1] &lt;&lt; 16) |
           (buf[offset + 2] &lt;&lt; 8) | buf[offset + 3];
  }
  return (buf[offset + 3] &lt;&lt; 24) | (buf[offset + 2] &lt;&lt; 16) |
         (buf[offset + 1] &lt;&lt; 8) | buf[offset];
}


Very simple bit shifting, about double the instructions as the first method, and can get a bit messy with larger data types. I presume this is the safer solution though, and perhaps more readable.

Is the first method OK, or would you consider it to be poor code in comparison to the second method? Or maybe there's just a better method all together (std::byte?).

Here is a compiler explorer link with both examples.
You are correct to be concerned about the first method. It generates UB if buf[offset] doesn't happen to be at the right alignment boundary for a 32-bit value.

The second method is the way to go, even if you don't care about portability (but as a bonus, it's portable, except for the use of int32_t).

As mentioned by @AJNeufeld you need to fix up your out-of-bound checks. Once that's done, you could simplify the function quite a bit by using std::swap(). For example:

if (bswap) {
    swap(buf[offset    ], buf[offset + 3]);
    swap(buf[offset + 1], buf[offset + 2]);
}
return (buf[offset + 3] &lt;&lt; 24) | (buf[offset + 2] &lt;&lt; 16) |
    (buf[offset + 1] &lt;&lt; 8) | buf[offset];


That's even pretty easy to generalize to any size with a couple of loops (that will almost certainly be unrolled).

EDIT: As @hoffmale notes in the comments, the return line is still possibly UB. The reason it technical.

auto b = int8_t{};
auto r = b &lt;&lt; 8;


The above code is legal because in every arithmetic operation, signed chars and shorts are always promoted to int. So b is promoted to int, which must be at least 16 bits, and the shift is cool, and r is int.

The problems arise when you shift by more than 8 bits. The types will always be promoted to int, but int is only guaranteed to be 16 bits. That means if you did:

auto r = b &lt;&lt; 16; // or 24


now you have a problem.

To solve it, you need to make sure the value being shifted is at least 32 bits before the shift:

auto r = uint_fast32_t{b} &lt;&lt; 16; // or 24


This works portably.

So you'd need to add this cast before each shift in the return line.

But a better way to handle this - one that scales - is to use a loop:

auto ret = int32_t{};
for (auto i = 0; i &lt; 4; ++i)
{
    ret &lt;&lt; 8;
    ret |= buf[offset + (3 - i)];
}


or even:

auto ret = int32_t{};
for (auto i = 0; i &lt; 4; ++i)
{
    ret &lt;&lt; 8;
    if (bswap)
        ret |= buf[offset + i];
    else
        ret |= buf[offset + (3 - i)];
}


and now you don't have to worry about promotions. Any decent optimizing compiler will hoist the invariant conditional and offset out of the loop and unroll the loop (and eliminate the unnecessary shift). And you can even template the loop on the size and reuse it.

Shifting and OR-ing is the only portable way to get the little-endian data in buf into an integer for all machines.

@juhist describes another option: using memcpy(). That's not a portable solution for a number of technical reasons. First, you'll get different behaviour depending on the endianness of the machine. More technically, the machine may not always store integers the way you expect, and I'm not referring to endianness by that. You're dealing with signed values, but the type of signedness of the data may not be the same as the machine (for example, the data may be twos-complement, the machine may be signed magnitude). There may be padding bits and other weirdness going on that ends up meaning that a 32 bit value of 0xFFFFFFFF may not necessarily be represented in memory as 4 bytes of 0xFF. Or if your input data is "0x00 0x00 0x00 0xA0" which if memcpy()ed on a little endian machine will give you 0xA0000000 (that's its value, not its memory representation), which you assume is −2147483648... but that's assuming bit 31 is the sign bit, which is not necessarily true. Could be bit 0, for example, meaning this value is actually 1342177280. (Granted, if your data has a sign bit, you really need to handle it specifically in any case. That is, you'd need to calculate the high byte as something like buf[offset + 3] &amp; 0x7Fu, and multiply the result by −1 if buf[offset + 3] &amp; 0x80u... or you could dump the whole thing into an unsigned value, and then deal with the sign bit. But the point about the sign bit not being where you expect it still stands: 0x01 0x00 0x00 0x00, if memcpy()ed on a little-endian machine, could produce −2147483648 rather than 1... or anything else really.)

If performance really matters - and, I'll be blunt, if you're using this part of a serialization routine, it almost certainly doesn't; whatever time you spend decoding integers is going to be dwarfed by the I/O - you could write system-specific code. For example, if your system is little-endian, and you know the representation is the same as what's in the buffer, you could just memcpy(). If it's big-endian, maybe memcpy() and a swap intrinsic, or a loop that's basically reverse memcpy() (create the integer, then copy the bytes in reverse order). If it's other-endian or non-twos-complement or has some other quirck... well, you'd know how to deal with that in that case.

The bottom line is that shifting and OR-ing is the only portable solution (and you'd need to handle the sign bit properly). If you don't care about portability and you want speed, well, then the "right" answer depends entirely on 1) the data representation, and 2) your machine.