WrappingInteger (auto-wrapping integral type)


#1

While learning about ring buffers, I got tired of having to manually remember to wrap the read and write index every time I used them.

So, with the help of some folks in the C++ Help discord (https://discord.gg/J5hBe8F), I came up with this:

#include <iostream>
#include <limits>
template<typename IntegralType, IntegralType min, IntegralType max>
struct WrappingInteger
{
    //====================== Usage Requirements ====================// 
    static_assert(std::is_integral<IntegralType>::value, "Integral type required");
    static_assert( min < max, "min must be less than max" );
    static_assert( max-min < std::numeric_limits<IntegralType>::max() / 2, "max - min must be less than half of the maximum allowed value for Integral Type to prevent overflow");
    
    //====================== Prefix & PostFix ====================// 
    WrappingInteger& operator++() { ++val; return wrap(); }
    WrappingInteger  operator++(int) { auto tmp = *this; ++val; wrap(); return tmp; }
    WrappingInteger& operator--() { --val; return wrap();  }
    WrappingInteger  operator--(int) { auto tmp = *this; --val; wrap(); return tmp; }
    
    //====================== Addition Assignment =========================//
    template<typename OtherType>
    WrappingInteger& operator+=(const OtherType& other)
    {
        IntegralType temp = static_cast<IntegralType>(other);
        while( temp > range )
        {
            val += range;
            wrap();
            temp -= range;
        }
        val += temp;
        return wrap();
    }
    
    WrappingInteger& operator+=( const WrappingInteger& other) { val += other.value; return wrap(); }
    
    //====================== Subtraction Assignment =========================//
    template<typename OtherType>
    WrappingInteger& operator-=(const OtherType& other)
    {
        IntegralType temp = static_cast<IntegralType>(other);
        while( temp > range )
        {
            val -= range;
            wrap();
            temp -= range;
        }
        val -= temp;
        return wrap();
    }
    
    WrappingInteger& operator-=(const WrappingInteger& other) { val -= other.val; return wrap(); }
    
    //===============================================//
    ~WrappingInteger() = default;
    
    //======================= Constructors ========================//
    /** allows you to specify a default value, which will be wrapped to remain within the specified range */
    WrappingInteger(IntegralType v = min) : val(v) { wrap(); }
    
    WrappingInteger(const WrappingInteger& other) = default;
    
    template<typename OtherIntegralType>
    WrappingInteger(const OtherIntegralType& otherType) { val = otherType; wrap(); }
    
    //==================== Assignment Operator ===========================//
    WrappingInteger& operator=(const WrappingInteger& other) = default;
    
    template<typename OtherIntegralType>
    WrappingInteger& operator=(const OtherIntegralType& otherType) { val = otherType; return wrap(); }
    
    //==================== Explicit Conversion and Getters ===========================//
    //allows static_cast<int>(WrappedInteger<T, min, max>) and prevents implicit conversion to IntegralType
    explicit operator IntegralType() const { return val; }
    IntegralType value() const { return val; }
    
    //===================== Addition & Subtraction ==========================//
    //these allow for buffer[idx + 3]; and buffer[3 + idx] to be used, where idx is a WrappedInteger
    friend WrappingInteger operator+(WrappingInteger lhs, WrappingInteger rhs) { return lhs += rhs; }
    friend WrappingInteger operator-(WrappingInteger lhs, WrappingInteger rhs) { return lhs -= rhs; }
    
    //===================== Range info ==========================//
    static constexpr auto rangeMin() { return min; }
    static constexpr auto rangeMax() { return max; }
private:
    WrappingInteger& wrap()
    {
        val -= min;//shift everything so the min/max is 0, (max-min)
        while( val < 0 ) { val += range; }
        val %= (range);
        val += min; //shift everything back so min/max is min, max
        return *this;
    }
    IntegralType val = min;
    static constexpr IntegralType range = max - min;
};

Here’s an example usage:

I’m on the fence about the operator IntegralType being explicit or not, as it requires you do things like:

WrappingInteger<int, 2, 10> wi;
std::vector<int> a = {1,2,3,4,5,6};
std::cout << a[static_cast<int>(wi)+1] << std::endl; //prints '4'

but being explicit like that cures the ambiguous errors that result if that function is not marked as explictit and you tried to do a[wi + 1];


#2

Sometimes C++ can make programmers over complicate things a little. Or perhaps I completely misunderstand!! :slight_smile:
What people have done for decades is wrap unsigned integers naturally and shift down the result (>>). Or use i = (i+1) & 2047; or similar, you can use any add values. And adjust their tables to suit power 2 sizes.


#3

my preference goes to

if (++index >= size)
    index -= size;

:slight_smile:


#4

Yes, I’ve used that in reverbs which is faster when odd sizes are needed. Especially when memory cache is important.


#5

The problem is that you have to remember to do those tricks. Put your wrapping trick in a class so you don’t have to remember to do that. Then see what else you have to add to your class to make it behave like your primitive type did.


#6

the goal for me is to be able to use this syntax:

auto val = buffer[idx++];

and not have to think about remembering to wrap idx around.


#7

If you’re going to be using this thing in performance critical code and the index is only ever going to be incremented by one then you should use a subtraction, rather than a modulo, for the wrapping.


#8

Do you mean subtracting my one for the loop? No-one is suggesting doing a modulo because % has a divide in the process.


#9

No - I just spotted a modulo in the original post.


#10

Yes, I saw that too.
Unfortunately the price for that generic solution is, that you cannot know, if wrap() was called after an increment, or in a case, when val is close to std::numeric_limits<T>::max().

I would probably vote for this one too

while( val > range ) { val -= range; }

but, there is a potential penalty.
Probably one of the cases, where it is too simple to generalise it IMHO