4
\$\begingroup\$

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:

  1. 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?
  2. I've been very cavalier in my treatment of signed integers, since it's not quite clear to me what I should be doing.
  3. 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.
  4. If I specialize std::numeric_limits for this class - what should I set traps to?
\$\endgroup\$
10
  • 1
    \$\begingroup\$ Well, this certainly isn't going to be efficient. Clang is the only compiler that transforms this into anything reasonable for arbitrary byte widths. GCC sometimes does okay, as long as the type size is a natural one, but that seems to make the template class rather pointless. ICC seems to choke even when the class is instantiated for a natural-sized type. I wouldn't feel comfortable reviewing this code without a better understanding of your "some reason or another" reasons. It isn't clear to me why you wouldn't just use a union. Does this really need to be generic? \$\endgroup\$ Jan 3, 2017 at 12:53
  • \$\begingroup\$ @CodyGray: You've made several points and asked several questions... I'll answer in 2 comments. Indeed, GCC doesn't like the non-natural sizes for memcpy() at least (perhaps also memset()). 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\$
    – einpoklum
    Jan 3, 2017 at 12:58
  • \$\begingroup\$ @CodyGray: About my motivation - it might sound a bit vague to you, but here goes. I'm writing some templated decompression kernels in CUDA; and in some cases the compressed data is, semantically, a 3-byte integer. What this means is that, mostly, this code will not really be compiled by GCC, clang or ICC, but rather by nvcc. In fact, some of it will likely be replaced/specialized away, and at any rate no data will be on the stack. The code as-is will be used mostly for debugging/correctness verification on the host side. So I'm less- (but not un-) concerned about its performance... \$\endgroup\$
    – einpoklum
    Jan 3, 2017 at 12:59
  • 1
    \$\begingroup\$ A union would just allow you to do safe type punning (well, officially only in C, but it shouldn't be a problem in C++ either, practically speaking). So to get a 24-bit int, you'd have something like 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\$ Jan 3, 2017 at 13:07
  • 1
    \$\begingroup\$ No, that's exactly what I meant. You read using the native integer width of the target microprocessor. That's probably 32 bits. Then you mask off the extra byte that you don't want (whether it's the MSB or LSB depends on whether you're running on a little-endian or big-endian machine). Doing it that way is going to be a lot faster, but it is less generic because you'll have to write the bit-masking instructions out explicitly each time. But I mean, that's what I'd do. I think it's clearer and easier to reason about. \$\endgroup\$ Jan 3, 2017 at 13:42

1 Answer 1

2
\$\begingroup\$

There is only ever a reason to have a move-ctor / move-assignment if that is more efficient than copy-ctor / copy-assignment, or the only one of the two which is valid.

.assign is wrong.
You probably want to zero non-assigned bytes, and copy assigned bytes.
Currently, you zero and then overwrite some bytes and access out-of-bounds if the source is smaller than the destination.

You might want to write normal de- and en-coding code which has the same codepaths for little- as for big-endian.

// Example:
unsigned in;
unsigned char out[4];
for(int i = 0; i < 4; i++) {
    out[i] = in & 0xff;
    in >>= 8;
}
\$\endgroup\$
5
  • \$\begingroup\$ Also - is it appropriate (by site etiquette) for me to make the correction here in the question? Or am I supposed to leave the code as I first posted it? For now I made the correction a comment. \$\endgroup\$
    – einpoklum
    Jan 3, 2017 at 12:46
  • \$\begingroup\$ Won't compilers have a hard time optimizing the decoding/encoding code snippet you listed here? \$\endgroup\$
    – einpoklum
    Jan 3, 2017 at 13:05
  • \$\begingroup\$ Added example code. Site etiquette says you could at most add the fix below clearly marked. Regarding optimization, try it out, they should get it done. But you know about theory and practice... \$\endgroup\$ Jan 3, 2017 at 13:05
  • \$\begingroup\$ Also, with your suggestion, the type will be internally little-endian. But that means that a uint_t<4> is not indistinguishable from a uint32_t - and I do want that indistinguishability. \$\endgroup\$
    – einpoklum
    Jan 3, 2017 at 13:12
  • \$\begingroup\$ Well, then you will have to explicitly code up both little- and big-endian, no way around it. \$\endgroup\$ Jan 3, 2017 at 13:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.