2
\$\begingroup\$

This class is meant to work exactly as an unsigned integer should work, but it limits the value based on a set amount of bits.

/**
 * @brief Any amount of bits integer.
 * @tparam bits amount of bits
 * 
 * @note stores with size of uint64_t
*/
template <uint8_t bits>
requires((bits >= 1) && (bits <= 64))
class uintx_t {
protected:
    uint64_t Val;

    constexpr static inline uint64_t formatBits(
        uint64_t val,
        uint16_t bits2
    ) {return val & (UINT64_MAX >> (64-bits2));}

public:
    uintx_t(int64_t val) {
        Val = formatBits(val, bits);
    }
    template <uint8_t otherBits>
    uintx_t(const uintx_t<otherBits>& val) {
        Val = formatBits(val.getVal(), bits);
    }

    constexpr inline uint64_t getVal() const {return Val;}

    std::string toBin() const {
        std::string bin = std::bitset<bits>(Val).to_string();
        return bin;
    }
    
    inline void operator= (int64_t val) {
        Val = formatBits(val, bits);
    }
    template <uint8_t otherBits>
    inline void operator= (uintx_t<otherBits> val) {
        Val = formatBits(val.getVal(), bits);
    }
    inline bool operator== (int64_t val) {
        return (Val == val);
    }
    template <uint8_t otherBits>
    inline bool operator==(const uintx_t<otherBits>& val) {
        return (Val == val.getVal());
    }
    inline bool operator!= (int64_t val) {
        return (Val != val);
    }
    template <uint8_t otherBits>
    inline bool operator!=(const uintx_t<otherBits>& val) {
        return (Val != val.getVal());
    }
    inline bool operator> (int64_t val) {
        return (Val > val);
    }
    template <uint8_t otherBits>
    inline bool operator> (const uintx_t<otherBits>& val) {
        return (Val > val.getVal());
    }
    inline bool operator< (int64_t val) {
        return (Val < val);
    }
    template <uint8_t otherBits>
    inline bool operator< (const uintx_t<otherBits>& val) {
        return (Val < val.getVal());
    }
    inline bool operator>= (int64_t val) {
        return (Val >= val);
    }
    template <uint8_t otherBits>
    inline bool operator>= (const uintx_t<otherBits>& val) {
        return (Val >= val.getVal());
    }
    inline bool operator<= (int64_t val) {
        return (Val <= val);
    }
    template <uint8_t otherBits>
    inline bool operator<= (const uintx_t<otherBits>& val) {
        return (Val <= val.getVal());
    }

    inline uintx_t operator+ (int64_t val) {
        return uintx_t(formatBits(Val + val, bits));
    }
    template <uint8_t otherBits>
    inline uintx_t operator+ (const uintx_t<otherBits>& val) {
        return uintx_t(formatBits(Val + val.getVal(), bits));
    }
    inline uintx_t operator- (int64_t val) {
        return uintx_t(formatBits(Val - val, bits));
    }
    template <uint8_t otherBits>
    inline uintx_t operator- (const uintx_t<otherBits>& val) {
        return uintx_t(formatBits(Val - val.getVal(), bits));
    }
    inline uintx_t operator* (int64_t val) {
        return uintx_t(formatBits(Val * val, bits));
    }
    template <uint8_t otherBits>
    inline uintx_t operator* (const uintx_t<otherBits>& val) {
        return uintx_t(formatBits(Val * val.getVal(), bits));
    }
    inline uintx_t operator/ (int64_t val) {
        return uintx_t(formatBits(Val / val, bits));
    }
    template <uint8_t otherBits>
    inline uintx_t operator/ (const uintx_t<otherBits>& val) {
        return uintx_t(formatBits(Val / val.getVal(), bits));
    }
    inline uintx_t operator% (int64_t val) {
        return uintx_t(formatBits(Val % val, bits));
    }
    template <uint8_t otherBits>
    inline uintx_t operator% (const uintx_t<otherBits>& val) {
        return uintx_t(formatBits(Val % val.getVal(), bits));
    }
    inline uintx_t operator| (int64_t val) {
        return uintx_t(formatBits(Val | val, bits));
    }
    template <uint8_t otherBits>
    inline uintx_t operator| (const uintx_t<otherBits>& val) {
        return uintx_t(formatBits(Val | val.getVal(), bits));
    }
    inline uintx_t operator& (int64_t val) {
        return uintx_t(formatBits(Val & val, bits));
    }
    template <uint8_t otherBits>
    inline uintx_t operator& (const uintx_t<otherBits>& val) {
        return uintx_t(formatBits(Val & val.getVal(), bits));
    }
    inline uintx_t operator^ (int64_t val) {
        return uintx_t(formatBits(Val ^ val, bits));
    }
    template <uint8_t otherBits>
    inline uintx_t operator^ (const uintx_t<otherBits>& val) {
        return uintx_t(formatBits(Val ^ val.getVal(), bits));
    }
    inline uintx_t operator<< (int64_t val) {
        return uintx_t(formatBits(Val << val, bits));
    }
    template <uint8_t otherBits>
    inline uintx_t operator<< (const uintx_t<otherBits>& val) {
        return uintx_t(formatBits(Val << val.getVal(), bits));
    }
    inline uintx_t operator>> (int64_t val) {
        return uintx_t(formatBits(Val >> val, bits));
    }
    template <uint8_t otherBits>
    inline uintx_t operator>> (const uintx_t<otherBits>& val) {
        return uintx_t(formatBits(Val >> val.getVal(), bits));
    }

    inline uintx_t& operator++ () {
        Val = formatBits(++Val, bits);
        return *this;
    }
    inline uintx_t& operator-- () {
        Val = formatBits(--Val, bits);
        return *this;
    }
    inline uintx_t operator++ (int) {
        Val = formatBits(++Val, bits);
        return *this;
    }
    inline uintx_t operator-- (int) {
        Val = formatBits(--Val, bits);
        return *this;
    }

    inline uintx_t operator+= (int64_t val) {
        Val = formatBits(Val+val, bits);
        return *this;
    }
    template <uint8_t otherBits>
    inline uintx_t operator+= (const uintx_t<otherBits>& val) {
        Val = formatBits(Val+val.getVal(), bits);
        return *this;
    }
    inline uintx_t operator-= (int64_t val) {
        Val = formatBits(Val-val, bits);
        return *this;
    }
    template <uint8_t otherBits>
    inline uintx_t operator-= (const uintx_t<otherBits>& val) {
        Val = formatBits(Val-val.getVal(), bits);
        return *this;
    }
    inline uintx_t operator*= (int64_t val) {
        Val = formatBits(Val*val, bits);
        return *this;
    }
    template <uint8_t otherBits>
    inline uintx_t operator*= (const uintx_t<otherBits>& val) {
        Val = formatBits(Val*val.getVal(), bits);
        return *this;
    }
    inline uintx_t operator/= (int64_t val) {
        Val = formatBits(Val/val, bits);
        return *this;
    }
    template <uint8_t otherBits>
    inline uintx_t operator/= (const uintx_t<otherBits>& val) {
        Val = formatBits(Val/val.getVal(), bits);
        return *this;
    }
    inline uintx_t operator%= (int64_t val) {
        Val = formatBits(Val%val, bits);
        return *this;
    }
    template <uint8_t otherBits>
    inline uintx_t operator%= (const uintx_t<otherBits>& val) {
        Val = formatBits(Val%val.getVal(), bits);
        return *this;
    }
    inline uintx_t operator|= (int64_t val) {
        Val = formatBits(Val|val, bits);
        return *this;
    }
    template <uint8_t otherBits>
    inline uintx_t operator|= (const uintx_t<otherBits>& val) {
        Val = formatBits(Val|val.getVal(), bits);
        return *this;
    }
    inline uintx_t operator&= (int64_t val) {
        Val = formatBits(Val&val, bits);
        return *this;
    }
    template <uint8_t otherBits>
    inline uintx_t operator&= (const uintx_t<otherBits>& val) {
        Val = formatBits(Val&val.getVal(), bits);
        return *this;
    }
    inline uintx_t operator^= (int64_t val) {
        Val = formatBits(Val^val, bits);
        return *this;
    }
    template <uint8_t otherBits>
    inline uintx_t operator^= (const uintx_t<otherBits>& val) {
        Val = formatBits(Val^val.getVal(), bits);
        return *this;
    }
    inline uintx_t operator<<= (int64_t val) {
        Val = formatBits(Val<<val, bits);
        return *this;
    }
    template <uint8_t otherBits>
    inline uintx_t operator<<= (const uintx_t<otherBits>& val) {
        Val = formatBits(Val<<val.getVal(), bits);
        return *this;
    }
    inline uintx_t operator>>= (int64_t val) {
        Val = formatBits(Val>>val, bits);
        return *this;
    }
    template <uint8_t otherBits>
    inline uintx_t operator>>= (const uintx_t<otherBits>& val) {
        Val = formatBits(Val>>val.getVal(), bits);
        return *this;
    }

    friend std::ostream& operator<< (
        std::ostream& os,
        uintx_t& UX
    ) {
        os << std::to_string(UX.Val);
        return os;
    }

    template <typename T>
    explicit operator T() const {
        return (T)Val;
    }
};

Example:

uintx_t<4> nibble = 3;
uintx_t<8> other = 20;

std::cout << nibble << '\n';
std::cout << nibble.toBin() << '\n';
std::cout << nibble + 4 << '\n';
std::cout << nibble + other << '\n';

Output:

3
0011
7
7
\$\endgroup\$
3
  • 4
    \$\begingroup\$ Can you explain the last example output? How come 3+20=9? That's 23 and 23 mod 16 is just 7. Where is 9 coming from? \$\endgroup\$
    – slepic
    May 5 at 4:23
  • \$\begingroup\$ Good point, I guess there's an issue with the way I have them added together. \$\endgroup\$ May 5 at 19:20
  • \$\begingroup\$ Oh that's my mistake actually, I just redid it without changing anything and the output is 7. \$\endgroup\$ May 5 at 20:41

1 Answer 1

2
\$\begingroup\$

You forgot std::

Types like uint8_t from <cstdint> are defined in the std namespace. Often these types are pulled into the global namespace as well because C headers are included behind the scenes, but you cannot rely on that. You should therefore write std::uint8_t in full.

Mixing signed and unsigned integers

I see both std::uint64_t and std::int64_t being used. C++ will silently convert these values into each other (unless you enable compiler warnings like -Wsign-conversion), but the result might not be what you expect. Since C++20, signed integers are guaranteed to be represented using two's complement, and that causes most of the implicit conversions in your code to be safe, it's better not to rely on that, and use std::uint64_t everywhere.

Naming things

It's weird to see both Val and val in your code. While C++ is case-sensitive, virtually all code styles use identifiers starting with lower-case for variables and functions. If you want to distinguish between a member variable and a function argument, I recommend you use m_ as a prefix for member variables, and perhaps s_ for static member variables.

I would also avoid unnecessary abbreviations, and just write value instead of val.

Conversion back to an integer

You wrote a templated conversion function, which at least is made explicit. However, inside it will do a C-style cast. This will allow silent casting of the value into types that might not be big enough to hold the value, or to pointers, and to any other type that can be created from an integer. Are you sure you want this to happen?

Instead of having a conversion operator, I would use getVal() (perhaps renamed to to_uint()), but modified to returns an integer of the smallest type with at least bits bits. Then the result can be implicitly casted to other types (which is safer, especially with the appropriate compiler warnings enabled) or the programmer can do some explicit casts on that if they really wanted to. Something like:

auto to_uint() const {
    if constexpr (bits <= 8)  return std::uint8_t(m_value);
    if constexpr (bits <= 16) return std::uint16_t(m_value);
    …
}

Create a mask instead of using formatBits()

Instead of having a static member function to mask the bits of a value, you can create a static constant holding the mask:

template <std::uint8_t bits>
requires((bits >= 1) && (bits <= 64))
class uintx_t {
protected:
    std::uint64_t m_value;
    static constexpr std::uint64_t s_mask = UINT64_MAX >> (64 - bits);

public:
    uintx_t(std::uint64_t value): m_value(value & s_mask) {}
    …
};

What about adding a uintx_t to a regular integer type?

You have two overloads for all the operators. One for adding a uintx_t to another uintx_t, and one for adding a regular integer to a uintx_t. But what if I wanted to add a uintx_t to a regular integer? The following does not compile:

uintx_t<4> nibble = 3;
auto foo = 4 + nibble; // compile error

Obviously you cannot solve this by using member functions. However, you can write a free function to do this:

template <std::uint8_t bits>
static constexpr std::uint64_t operator+(std::uint64_t lhs, uintx_t<bits> rhs) {
    return lhs + rhs.to_uint();
}

Note the return type. It's not the same as your operator+(int64_t), which returns a uintx_t<bits>. What if you wrote:

uintx_t<4> nibble = 3;
std::uint64_t large_result = nibble + 20;

Do you expect large_result to be 23 or 7? I would personally say the latter. This matches how regular integers behave; they are automatically promoted to the larger type. And since addition is commutative, you expect the order of the arguments not to matter.

Reducing code duplication

Already you have two overloads for every operator, and as shown that doesn't cover all the possibilties of mixing regular integers and uintx_ts. You probably don't want to add even more overloads. You could consider find a way to reduce the duplication, for example by writing an even more generic operator overload like so:

auto operator+(Uint auto lhs, Uint auto rhs)
     -> std::common_type<decltype(lhs), decltype(rhs)>
{
    return value_of(lhs) + value_of(rhs);
}

Where Uint is then a concept that matches both std::uint64_t and your uintx_t. Then there is a helper function value_of(), which just returns the input if it's a regular integer, and returns the result of .to_uint() if it's a uintx_t. You want to overload std::common_type so the common type of two uintx_ts is the larger uintx_t, and std::uint64_t if one of the sides is std::uint64_t.

\$\endgroup\$
2
  • \$\begingroup\$ If I use uint64_t in my operators instead of int64_t, wont that lead to messed up results if a negative number is used in an operator, since it will convert the negative number into a positive one? \$\endgroup\$ May 6 at 13:56
  • 1
    \$\begingroup\$ @RhinoRunner that already happens since you're doing an operation between an int64_t and an uint64_t (and the C++ semantics for that are "convert the signed integer to unsigned, then do the operation"). Also, the corresponding positive number is the unique number such that it acts as if it is the original negative number in most cases (eg adding -1 and adding 0xFFFFFFFFFFFFFFFF has the same effect anyway). Your comparisons may be broken, if you expect the signedness to be relevant there. \$\endgroup\$
    – harold
    May 6 at 14:33

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.