我如何静态断言在非模板成员函数中禁止 "mixed endianness"

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

我在包含 std::array<std::byte, 20>.[=21= 的结构中的 operator<=> 的高性能实现中使用 2 x std::uint64_t 和 1 x std::uint32_t ]

我正在尝试使其跨编译器和架构兼容。

作为其中的一部分,我试图彻底拒绝任何 std::endian::native 不是 std::endian::littlestd::endian::big.

的架构

我认为我 运行 违反了“static_assert 必须依赖模板参数规则”,因为结构和成员函数没有模板化。

  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");
    }
  }


我想我可以退而求其次,而不是 static_assert

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

我建议断言它是大 小端:

#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 ...;
        }
    }
};

整理后的版本,基于反馈,还添加了一点抽象:

#ifdef __cpp_lib_byteswap
using std::byteswap;
#else
template <class T>
constexpr T byteswap(T n) noexcept {
// clang-format off
  // NOLINTBEGIN
  #ifdef _MSC_VER
    #define BYTE_SWAP_16 _byteswap_ushort
    #define BYTE_SWAP_32 _byteswap_ulong
    #define BYTE_SWAP_64 _byteswap_uint64
  #else
    #define BYTE_SWAP_16 __builtin_bswap16
    #define BYTE_SWAP_32 __builtin_bswap32
    #define BYTE_SWAP_64 __builtin_bswap64
  #endif
  // NOLINTEND
  // clang-format on

  if constexpr (std::same_as<T, std::uint64_t>) {
    return BYTE_SWAP_64(n);
  } else if constexpr (std::same_as<T, std::uint32_t>) {
    return BYTE_SWAP_32(n);
  } else if constexpr (std::same_as<T, std::uint16_t>) {
    return BYTE_SWAP_16(n);
  }
}
#endif

template <typename T, typename... U>
concept any_of = (std::same_as<T, U> || ...);

// convert the sizeof(Target) bytes starting at `source` pointer to Target
// uses compiler intrinsics for endianess conversion if required and if `swap` == true
// caller responsibility to ensure that enough bytes are readable/dereferencable etc
// this compiles to a load and `bswap` which is very fast and can beat eg `memcmp`
template <typename Target, bool swap_if_required = true>
Target bytearray_cast(
    const std::byte* source) requires any_of<Target, std::uint64_t, std::uint32_t, std::uint16_t> {

  static_assert(std::endian::native == std::endian::big ||
                    std::endian::native == std::endian::little,
                "mixed-endianess architectures are not supported");

  Target value = *reinterpret_cast<const Target*>(source); // NOLINT

  if constexpr (swap_if_required && std::endian::native == std::endian::little) {
    return byteswap<Target>(value);
  } else {
    return value;
  }
}

template <typename T>
std::strong_ordering
three_way(const std::byte* a,
          const std::byte* b) requires any_of<T, std::uint64_t, std::uint32_t, std::uint16_t> {
  return bytearray_cast<T>(a) <=> bytearray_cast<T>(b);
}

template <typename T>
bool equal(const std::byte* a,
           const std::byte* b) requires any_of<T, std::uint64_t, std::uint32_t, std::uint16_t> {
  // don't bother swapping for endianess, since we don't need it
  return bytearray_cast<T, false>(a) == bytearray_cast<T, false>(b);
}

struct pawned_pw {

  std::strong_ordering operator<=>(const pawned_pw& rhs) const {
    if (auto cmp = three_way<std::uint64_t>(&hash[0], &rhs.hash[0]);
        cmp != std::strong_ordering::equal)
      return cmp;

    if (auto cmp = three_way<std::uint64_t>(&hash[8], &rhs.hash[8]);
        cmp != std::strong_ordering::equal)
      return cmp;

    return three_way<std::uint32_t>(&hash[16], &rhs.hash[16]);
  }

  bool operator==(const pawned_pw& rhs) const {
    if (bool cmp = equal<std::uint64_t>(&hash[0], &rhs.hash[0]); !cmp) return cmp;
    if (bool cmp = equal<std::uint64_t>(&hash[8], &rhs.hash[8]); !cmp) return cmp;
    return equal<std::uint32_t>(&hash[16], &rhs.hash[16]);
  }

  std::array<std::byte, 20> hash;
};