Search code examples
c++memorybitportabilitybitset

What is a generic and portable way to get at underlying bits of a type in C++?


I want to take an arbitrary type that is no bigger than a pointer and get at its bit representation. I tried to use std::bit_cast, but that requires both types are the same size. But, because of genericity, I don't know what size the input type will be. I wrote this function:

template<class To, class From>
To sloppy_bit_cast(From x) {
  To temp{0};
  std::memcpy(&temp, &x, std::min(sizeof(From), sizeof(To)));
  return temp;
}

and it works on my computer, but I am worried the memcpy call will behave differently based on endianness. Are there other portability pitfalls I need to worry about here? Do std::bitset's conversion functions solve this portably already?

Edit: specifically, I want to copy from least significant bit to least significant bit.

If you're interested, here's the whole code as of now (I haven't tested it, but my linter accepts it and it feels right at least). I'm not asking you to read or review it (though I would be happy to hear feedback). But I'm so happy you all helped me solve this, so I wanted to share. I'm playing omitting the assertions for not just to get the general idea i out there, but I think I can work out that to constrain

#include <array>
#include <cstring>
#include <cstdint>


template<class T>
constexpr T narrow_cast(auto x) noexcept{
  return static_cast<T>(x);
}

template<class To, class From>
constexpr To bit_cast(From x) noexcept {
  static_assert(sizeof(To) == sizeof(From));
  To temp;
  std::memcpy(&temp, &x, sizeof(To));
  return temp;
}

template<class T>
constexpr auto bytes_of(T x) noexcept {
  return bit_cast<std::array<std::byte, sizeof x>>(x);
}

template<class T>
constexpr T to_integer(auto x) noexcept{
  return static_cast<T>(x);
}

template<class UInt, class T>
constexpr auto as_UInt(T x) noexcept {
  auto const bytes = bytes_of(x);
  UInt acc{};
  //endian agnostic
  for (auto i = 0u; i < bytes.size(); ++i) {
    auto const this_byte = to_integer<UInt>(bytes[i]);
    acc |= (this_byte << (i * 8u));
  }
  return acc;
}

template<class To, class UInt>
constexpr auto from_UInt(UInt from) noexcept {
  std::array<std::byte,sizeof(To)> bytes;
  //endian agnostic?
  for (auto i = 0u; i < bytes.size(); ++i){
    bytes[i] = narrow_cast<std::byte>((from >> (i * 8u)) & 0xFFu);
  }
  return bit_cast<To>(bytes);
}

template<class T>
constexpr auto as_uintptr_t(T x) noexcept {
  return as_UInt<uintptr_t>(x);
}

template<class To>
constexpr auto from_uintptr_t(uintptr_t x) noexcept {
  return from_UInt<To>(x);
}

template<class X, class Y, int low_bit_count_ = sizeof(Y) * 8>
class uintptr_pair {
  static_assert(sizeof(X) <= sizeof(uintptr_t));
  static_assert(sizeof(Y) <= sizeof(uintptr_t));

 public:
  static constexpr auto low_bit_count = low_bit_count_;
  static constexpr auto high_bit_count = sizeof(uintptr_t)*8 - low_bit_count;
  constexpr uintptr_pair() = default;
  constexpr uintptr_pair(X x, Y y) noexcept
      : x_{as_uintptr_t(x)}, y_{as_uintptr_t(y)} {}

  constexpr X x() const noexcept { return from_uintptr_t<X>(x_); }
  constexpr Y y() const noexcept { return from_uintptr_t<Y>(y_); }

 private:
  uintptr_t x_ : high_bit_count;
  uintptr_t y_ : low_bit_count;
};

constexpr auto test() {
  uintptr_pair<int, int> p{3, 4};
  return std::pair{p.x(),p.y()};
}

Solution

  • You can trivially arrange to have the correct amount of memory:

    template<class T>
    auto get_bytes(const T &t) {
      std::array<std::byte,sizeof t> ret;
      std::memcpy(ret.data(),&t,sizeof t);
      return ret;
    }
    

    This works even if T is not trivially-copyable, although you then can’t put the bytes back in a T. (std::bit_cast<std::array<…>> would probably work, but there’s no true guarantee that the size of the class would be correct.)

    If you want the result as an integer, you can pick one of at least as many bytes and then either fill it yourself via | and << (which produces a value to analyze independent of issues like endianness) or memcpy into it from an array that you filled as above. Again, if T has fewer bytes, theoretically the behavior isn’t guaranteed, but it will certainly work in practice.