I've designed a single-file safe integer library in C++. It catches undefined behavior prior to integer overflows or underflows and throws the respective exceptions. I intend this to be portable, to not rely on undefined behavior, and to rely as little on implementation-defined behavior as possible.
safe_integer.hpp
:
/*
* Copyright © 2020 James Larrowe
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef SAFE_INTEGER_HPP
# define SAFE_INTEGER_HPP 1
#include <limits>
#include <stdexcept>
#include <type_traits>
template<typename I,
typename std::enable_if<std::is_integral<I>::value, bool>::type = true>
class safe_int
{
I val;
public:
typedef I value_type;
static constexpr I max = std::numeric_limits<I>::max();
static constexpr I min = std::numeric_limits<I>::min();
safe_int(I i) : val { i } { };
operator I() const { return val; }
I &operator=(I v) { val = v; }
safe_int operator+()
{
return *this;
}
safe_int operator-()
{
if(val < -max)
throw std::overflow_error("");
return safe_int(-val);
}
safe_int &operator++()
{
if(val == max)
throw std::overflow_error("");
++val;
return *this;
}
safe_int &operator--()
{
if(val == min)
throw std::underflow_error("");
--val;
return *this;
}
safe_int operator++(int)
{
if(val == max)
throw std::overflow_error("");
return safe_int(val++);
}
safe_int operator--(int)
{
if(val == min)
throw std::underflow_error("");
return safe_int(val--);
}
safe_int &operator+=(I rhs)
{
if( val > 0 && rhs > max - val )
throw std::overflow_error("");
else if( val < 0 && rhs < min - val )
throw std::underflow_error("");
val += rhs;
return *this;
}
safe_int &operator-=(I rhs)
{
if( val >= 0 && rhs < -max )
throw std::overflow_error("");
if( val < 0 && rhs > max + val )
throw std::overflow_error("");
else if( val > 0 && rhs < min + val )
throw std::underflow_error("");
val -= rhs;
return *this;
}
safe_int &operator*=(I rhs)
{
if(val > 0)
{
if(rhs > max / val)
throw std::overflow_error("");
}
else if(val < 0)
{
if(val == -1)
{
if(rhs < -max)
throw std::overflow_error("");
goto no_overflow;
}
if(rhs > min / val)
throw std::underflow_error("");
}
no_overflow:
val *= rhs;
return *this;
}
safe_int &operator/=(I rhs)
{
if( rhs == -1 && val < -max )
throw std::underflow_error("");
else if(rhs == 0)
throw std::domain_error("");
val /= rhs;
return *this;
}
safe_int &operator%=(I rhs)
{
if( rhs == -1 && val < -max )
throw std::underflow_error("");
else if(rhs == 0)
throw std::domain_error("");
val %= rhs;
return *this;
}
safe_int operator+(I rhs)
{
return safe_int(val) += rhs;
}
safe_int operator-(I rhs)
{
return safe_int(val) -= rhs;
}
safe_int operator*(I rhs)
{
return safe_int(val) *= rhs;
}
safe_int operator/(I rhs)
{
return safe_int(val) /= rhs;
}
safe_int operator%(I rhs)
{
return safe_int(val) %= rhs;
}
safe_int &operator+=(safe_int rhs)
{
return *this += static_cast<I>(rhs);
}
safe_int &operator-=(safe_int rhs)
{
return *this -= static_cast<I>(rhs);
}
safe_int &operator*=(safe_int rhs)
{
return *this *= static_cast<I>(rhs);
}
safe_int &operator/=(safe_int rhs)
{
return *this /= static_cast<I>(rhs);
}
safe_int &operator%=(safe_int rhs)
{
return *this %= static_cast<I>(rhs);
}
safe_int operator+(safe_int rhs)
{
return safe_int(val) += static_cast<I>(rhs);
}
safe_int operator-(safe_int rhs)
{
return safe_int(val) -= static_cast<I>(rhs);
}
safe_int operator*(safe_int rhs)
{
return safe_int(val) *= static_cast<I>(rhs);
}
safe_int operator/(safe_int rhs)
{
return safe_int(val) /= static_cast<I>(rhs);
}
safe_int operator%(safe_int rhs)
{
return safe_int(val) %= static_cast<I>(rhs);
}
};
#endif
This should work on non-two's complement systems and with any integer type.
Here's a little example:
#include "safe_integer.hpp"
int main(void)
{
safe_int<int> i = 0;
i -= -0x80000000;
return 0;
}
Output:
terminate called after throwing an instance of 'std::overflow_error'
what():
Aborted
What I'm particularly interested in:
Are there any corner cases I've missed?
Is there any undefined behavior (probably not)?
Is there any way I can simplify all of the (somewhat redundant) operator overloads?
What I'm not interested in:
- efficiency. I agree that my solution may not perform well but my personal opinion is that leaving the checks in unconditionally and not using undefined behavior to get a faster result are worth the cost.