For some reason or another, I want to have available unsigned integers of sizes other than 1, 2, 4 and 8 (e.g. an unsigned integer with 3 bytes). For most platforms, compilers don't make those available; so - I rolled my own.
Other than asking for a general review, I will also pose a few specific questions / requests for guidance.
#ifndef UINT_H_
#define UINT_H_
#include <boost/integer.hpp>
#include <climits>
#include <ostream>
#include <istream>
#include <cstring> // for memcpy and memset
namespace util {
/**
* A hopefully-fast integer-like class with arbitrary size
*
* @note Heavily dependent on compiler optimizations...
* @note For now, assumes little-endianness
* @note For now, limited to small sizes
*
*/
template <unsigned N>
class uint_t final
{
static_assert(N <= sizeof(unsigned long long), "Size not supported, for now");
public: // types and constants
enum { num_bytes = N, num_bits = N * CHAR_BIT };
using byte = unsigned char;
using value_type = byte[N];
using fast_builtin_type = typename boost::int_t<num_bits>::fast;
using least_builtin_type = typename boost::int_t<num_bits>::least;
protected: // data members
value_type value; // Note it is _not_ necessarily aligned
public: // constructors
uint_t() noexcept = default;
uint_t(const uint_t& x) noexcept = default;
uint_t(uint_t&& x) noexcept = default;
protected: // building blocks for converting ctors, assignments and conversion operators
/* The next two methods are buggy, see @Deduplicator's answer */
template <typename I>
uint_t& assign (I x) noexcept
{
if (sizeof(I) < N) {
std::memset(value, sizeof(uint_t) - sizeof(I), 0);
}
std::memcpy(value, &x, N);
return *this;
}
template <typename I>
I as_integer() const noexcept
{
I result;
if (sizeof(I) < N) { result = 0; }
std::memcpy(&result, value, N);
return result;
}
/*
// Alternative for the two above methods,
// following @Deduplicator's answer:
static constexpr size_t min(size_t x, size_t y) { return x < y ? x : y; }
template <typename I>
uint_t& assign(I x) noexcept
{
auto x_bytes = (const byte* const) &x;
for (auto j = 0; j < min(sizeof(I), N); j++) {
value[j] = x_bytes[j];
}
for (auto j = min(sizeof(I), N); j < N; j++) {
value[j] = 0;
}
return *this;
}
template <typename I>
I as_integer() const noexcept
{
I result;
if (sizeof(I) > N) { result = 0; }
auto result_bytes = (byte* const) &result;
for (auto j = 0; j < min(sizeof(I), N); j++) {
result_bytes[j] = value[j];
}
return result;
}
*/
public: // converting constructors
uint_t(char x) noexcept { assign<char >(x); }
uint_t(signed char x) noexcept { assign<signed char >(x); }
uint_t(unsigned char x) noexcept { assign<unsigned char >(x); }
uint_t(short x) noexcept { assign<short >(x); }
uint_t(unsigned short x) noexcept { assign<unsigned short >(x); }
uint_t(int x) noexcept { assign<int >(x); }
uint_t(unsigned x) noexcept { assign<unsigned >(x); }
uint_t(long x) noexcept { assign<long >(x); }
uint_t(unsigned long x) noexcept { assign<unsigned long >(x); }
uint_t(long long x) noexcept { assign<long long >(x); }
uint_t(unsigned long long x) noexcept { assign<unsigned long long >(x); }
~uint_t() = default;
public: // operators
uint_t& operator = (const uint_t& other) noexcept = default;
uint_t& operator = (uint_t&& other) noexcept = default;
uint_t& operator = (char x) noexcept { return assign<char >(x); }
uint_t& operator = (signed char x) noexcept { return assign<signed char >(x); }
uint_t& operator = (unsigned char x) noexcept { return assign<unsigned char >(x); }
uint_t& operator = (short x) noexcept { return assign<short >(x); }
uint_t& operator = (unsigned short x) noexcept { return assign<unsigned short >(x); }
uint_t& operator = (int x) noexcept { return assign<int >(x); }
uint_t& operator = (unsigned x) noexcept { return assign<unsigned >(x); }
uint_t& operator = (long x) noexcept { return assign<long >(x); }
uint_t& operator = (unsigned long x) noexcept { return assign<unsigned long >(x); }
uint_t& operator = (long long x) noexcept { return assign<long long >(x); }
uint_t& operator = (unsigned long long x) noexcept { return assign<unsigned long long >(x); }
uint_t& operator += (const fast_builtin_type& other) noexcept { return *this = as_fast_builtin() + other; }
uint_t& operator -= (const fast_builtin_type& other) noexcept { return *this = as_fast_builtin() - other; }
uint_t& operator *= (const fast_builtin_type& other) noexcept { return *this = as_fast_builtin() * other; }
uint_t& operator /= (const fast_builtin_type& other) { return *this = as_fast_builtin() / other; }
uint_t& operator += (const uint_t& other) noexcept { return operator+=(other.as_fast_builtin()); }
uint_t& operator -= (const uint_t& other) noexcept { return operator-=(other.as_fast_builtin()); }
uint_t& operator *= (const uint_t& other) noexcept { return operator*=(other.as_fast_builtin()); }
uint_t& operator /= (const uint_t& other) { return operator/=(other.as_fast_builtin()); }
bool operator == (const uint_t& other) noexcept { return value == other.value; }
bool operator != (const uint_t& other) noexcept { return value != other.value; }
public: // conversion operators
operator fast_builtin_type() const noexcept { return as_integer<fast_builtin_type>(); }
public: // non-mutator methods
fast_builtin_type as_fast_builtin() const noexcept { return as_integer<fast_builtin_type>(); }
fast_builtin_type as_least_builtin() const noexcept { return as_integer<least_builtin_type>(); }
};
// Additional operators which can make do with public members
template <unsigned N> bool operator > (const uint_t<N>&x, const uint_t<N>& y) noexcept { return x.as_fast_builtin() > y.as_fast_builtin(); }
template <unsigned N> bool operator < (const uint_t<N>&x, const uint_t<N>& y) noexcept { return x.as_fast_builtin() < y.as_fast_builtin(); }
template <unsigned N> bool operator >= (const uint_t<N>&x, const uint_t<N>& y) noexcept { return x.as_fast_builtin() >= y.as_fast_builtin(); }
template <unsigned N> bool operator <= (const uint_t<N>&x, const uint_t<N>& y) noexcept { return x.as_fast_builtin() <= y.as_fast_builtin(); }
template <unsigned N> uint_t<N>& operator ++ (uint_t<N>& i) noexcept { return (i += 1); }
template <unsigned N> uint_t<N>& operator -- (uint_t<N>& i) noexcept { return (i -= 1); }
template <unsigned N>
uint_t<N> operator ++ (uint_t<N>& i, int) noexcept
{
uint_t<N> result = i;
i += 1;
return result;
}
template <unsigned N>
uint_t<N> operator -- (uint_t<N>& i, int) noexcept
{
uint_t<N> result = i;
i -= 1;
return result;
}
template <unsigned N>
std::ostream& operator<<(std::ostream& os, uint_t<N> i) { return os << i.as_least_builtin(); }
template <unsigned N>
std::istream& operator>>(std::istream& is, uint_t<N> i)
{
typename uint_t<N>::fast_builtin_type fast_builtin;
is >> fast_builtin;
i = fast_builtin;
return is;
}
} // namespace util
#endif /* UINT_H_ */
My questions/requests for guidance:
- My implementation currently assumes little-endianness. What would you suggest as the 'proper' way to support big-endian platforms? Another template parameter? preprocessor directives? Something else?
- I've been very cavalier in my treatment of signed integers, since it's not quite clear to me what I should be doing.
- Should I try and optimize the memcpy() myself? e.g. with a large switch statement over
N
(the number of bytes), which perhaps also accounts for the alignment or mis-alignment of the data? I was thinking about that and noticed different behavior in different compilers. - If I specialize
std::numeric_limits
for this class - what should I settraps
to?
memcpy()
at least (perhaps alsomemset()
). But that doesn't invalidate the design; with a custom memcpy here (perhaps coded separately for various sizes) that could be overcome, I guess. About using a union - what would I use a union for? And a union of what? \$\endgroup\$union { struct { uint8_t a; uint8_t b; uint8_t c; }; struct { uint16_t ab; uint8_t c; }; };
But judging from your explanation of the use-case, I would just treat the value that is semantically 24 bits as being of a native-width size (e.g., 32 bits), and then mask off the bits that need to be ignored. That's going to be a lot faster, albeit less generic. \$\endgroup\$