Using a union to resolve compiler warning: dereferencing type-punned pointer will break strict-aliasing rules

76 Views Asked by At

The following code results in the compiler warning:

dereferencing type-punned pointer will break strict-aliasing rules

#include <endian.h>

template <typename T>                                                 
inline T HostToLittleEndian(T val) {                                              
  switch (sizeof(val)) {                                                   
    case 1:                                                                
      return val;                                                          
    case 2: {                                                              
      uint16_t r = htole16(*reinterpret_cast<uint16_t*>(&val));                
      return *reinterpret_cast<T*>(&r);                                    
    }                                                                      
    case 4: {                                                              
      uint32_t r = htole32(*reinterpret_cast<uint32_t*>(&val));                
      return *reinterpret_cast<T*>(&r);                                    
    }                                                                      
    case 8: {                                                              
      uint64_t r = htole64(*reinterpret_cast<uint64_t*>(&val));                
      return *reinterpret_cast<T*>(&r);                                    
    }                                                                      
    default:                                                               
      static_assert(sizeof(val) <= 8, "Value is 64-bits or less.");        
  }                                                                        
}                                                                          

I was able to refactor using a union, which resolves the compiler warning, but now there's an extra copy of val. This other StackOverflow question makes me believe the copy is unavoidable, is this true? Or is there a more optimal solution I'm failing to realize?

template <typename T>
inline T HostToLittleEndian(T val) {
  union {
    T value;
    uint64_t u64;
    uint32_t u32;
    uint16_t u16;
    uint8_t u8;
  } data;
  data.value = val;
  
  switch (sizeof(val)) {
    case 1:
      break;
    case 2:
      data.u16 = htole16(data.u16);
      break;
    case 4:
      data.u32 = htole32(data.u32);
      break;
    case 8:
      data.u64 = htole64(data.u64);
      break;
    default:
      static_assert(sizeof(val) <= 8, "Value is 64-bits or less.");
  }
  return data.value;   
}

Note, both integral and float types are valid T types.

1

There are 1 best solutions below

1
Remy Lebeau On BEST ANSWER

When T is an integral type, you don't really need the casts, as implicit conversions should work just fine, eg:

#include <type_traits>
#include <endian.h>

template <typename T>
inline T HostToLittleEndian(T val) {
  static_assert(std::is_integral<T>::value, "Value must be integral");
  switch (sizeof(val)) {
    case 1:
      return val;
    case 2: {
      return htole16(val);
    }
    case 4: {
      return htole32(val);
    }
    case 8: {
      return htole64(val);
    }
    default:
      static_assert(sizeof(val) <= 8, "Value must be 64-bits or less.");
  }
}

UPDATE:

To handle floating-point types, you should copy them into a local buffer, swap the bytes, and then copy back (see: swapping "endianness" of floats and doubles), eg:

#include <type_traits>
#include <endian.h>

template <typename T>
inline T HostToLittleEndian(T val) {
  static_assert(std::is_arithmetic_v<T>, "Value must be arithmetic");
  switch (sizeof(val)) {
    case 1:
      return val;
    case 2: {
      return htole16(val);
    }
    case 4: {
      if constexpr (std::is_floating_point_v<T>) {
        uint32_t tmp;
        std::memcpy(&tmp, &val, 4);
        tmp = htole32(tmp);
        std::memcpy(&val, &tmp, 4);
        return val;
        // or, in C++20 and later:
        // #include <bit>
        // return std::bit_cast<T>(htole32(std::bit_cast<uint32_t>(val)));
      }
      else {
        return htole32(val);
      }
    }
    case 8: {
      if constexpr (std::is_floating_point_v<T>) {
        uint64_t tmp;
        std::memcpy(&tmp, &val, 8);
        tmp = htole64(tmp);
        std::memcpy(&val, &tmp, 8);
        return val;
        // or, in C++20 and later:
        // #include <bit>
        // return std::bit_cast<T>(htole64(std::bit_cast<uint64_t>(val)));
      }
      else {
        return htole64(val);
      }
    }
    default:
      static_assert(sizeof(val) <= 8, "Value must be 64-bits or less.");
  }
}

This will also work for integral types too, but that is not strictly necessary.