Search code examples
c++endiannessstatic-assertif-constexpr

How can I static assert to disallow "mixed endianness" in a non-templated member function


I am using 2 x std::uint64_t and 1 x std::uint32_t in a high performance implementation of of operator<=> in a struct conataining a std::array<std::byte, 20>.

I am trying to make it cross compiler and architecture compatible.

As part of that I am trying to outright reject any architecture with std::endian::native which is not std::endian::little or std::endian::big.

I think I am running foul of the "static_assert must depend on a template parameter rule", as the struct and the member function are not templated.

  std::strong_ordering operator<=>(const pawned_pw& rhs) const {

    static_assert(sizeof(std::uint64_t) == 8);
    static_assert(sizeof(std::uint32_t) == 4);

    if constexpr (std::endian::native == std::endian::little) {

      // c++23 will have std::byteswap, so we won't need this
#ifdef _MSC_VER
#define BYTE_SWAP_32 _byteswap_ulong
#define BYTE_SWAP_64 _byteswap_uint64
#else
#define BYTE_SWAP_32 __builtin_bswap32
#define BYTE_SWAP_64 __builtin_bswap64
#endif

      // this compiles to a load and `bswap` which should be fast
      // measured > 33% faster than hash < rhs.hash, which compiles to `memcmp`
      std::uint64_t head     = BYTE_SWAP_64(*(std::uint64_t*)(&hash[0]));     // NOLINT
      std::uint64_t rhs_head = BYTE_SWAP_64(*(std::uint64_t*)(&rhs.hash[0])); // NOLINT
      if (head != rhs_head) return head <=> rhs_head;

      std::uint64_t mid     = BYTE_SWAP_64(*(std::uint64_t*)(&hash[8]));     // NOLINT
      std::uint64_t rhs_mid = BYTE_SWAP_64(*(std::uint64_t*)(&rhs.hash[8])); // NOLINT
      if (mid != rhs_mid) return mid <=> rhs_mid;

      std::uint32_t tail     = BYTE_SWAP_32(*(std::uint32_t*)(&hash[16]));     // NOLINT
      std::uint32_t rhs_tail = BYTE_SWAP_32(*(std::uint32_t*)(&rhs.hash[16])); // NOLINT
      return tail <=> rhs_tail;
    } else if constexpr (std::endian::native == std::endian::big) {
      // can use big_endian directly
      std::uint64_t head     = *(std::uint64_t*)(&hash[0]);     // NOLINT
      std::uint64_t rhs_head = *(std::uint64_t*)(&rhs.hash[0]); // NOLINT
      if (head != rhs_head) return head <=> rhs_head;

      std::uint64_t mid     = *(std::uint64_t*)(&hash[8]);     // NOLINT
      std::uint64_t rhs_mid = *(std::uint64_t*)(&rhs.hash[8]); // NOLINT
      if (mid != rhs_mid) return mid <=> rhs_mid;

      std::uint32_t tail     = *(std::uint32_t*)(&hash[16]);     // NOLINT
      std::uint32_t rhs_tail = *(std::uint32_t*)(&rhs.hash[16]); // NOLINT
      return tail <=> rhs_tail;
    } else {
      static_assert(std::endian::native != std::endian::big &&
                    std::endian::native != std::endian::little,
                    "mixed-endianess architectures are not supported");
    }
  }


I guess I could just fall back instead of static_assert

    } else {
      // fall back to the slow way
      hash <=> rhs.hash;
    }


Solution

  • I suggest asserting that it's either big or little endian:

    #include <bit>
    #include <compare>
    
    struct pawned_pw {
        std::strong_ordering operator<=>(const pawned_pw& rhs) const {
            static_assert(std::endian::native == std::endian::big ||
                              std::endian::native == std::endian::little,
                          "mixed-endianess architectures are not supported");
    
            if constexpr (std::endian::native == std::endian::little) {
                return ...;
            } else {
                // big
                return ...;
            }
        }
    };