I wrote a C++ library to provide extra signed and unsigned integer types that saturate in overflow situations. It's in proof of concept stage and I'd love to get some feedback on it.
A short usage demo (usable in Compiler Explorer):
#include <cstddef>
#include <cstdint>
#include "saturating_types.hpp"
uint8_t x[] { 101, 27, 3, 95 };
int main () {
uint_sat8_t s = 25;
for (auto& v : x) {
s -= v;
} // s == 0
s++; // s == 1
for (const auto& v : x) {
s *= v;
}
volatile unsigned j = s; // s == 255
}
The library:
/**@file
* @brief Always saturating integer types.
*
* Some assumptions and notes:
* - The operators are 'viral', adding a saturating type and any other returns another saturating type.
* - Divide by zero clips the value to max()
* - Tries to avoid the normal promotion rules
* - The separate `add`, `substract`, etc functions can be used to define extra external operators
* returning saturated types.
*
* TODO: Further test and improve algorithms (hardware specific functions / reduce branching?)
* TODO: Enforce integral types where needed
* TODO: Enforce maximum base type size.
* TODO: Enhance interaction with floating point types
* TODO: Add toggle for `return_type` of operators (viral-ness)
*/
#pragma once
#include <cstdint>
#include <limits>
#include <utility>
#include <type_traits>
namespace {
// Helpers to convert small types to the next size up:
template <typename T, typename U, size_t S = (sizeof(T) > sizeof(U) ? sizeof(T) : sizeof(U)), bool B = std::is_unsigned<T>::value> struct next_up {};
template <typename T, typename U> struct next_up<T, U, 1, false> { typedef int type; };
template <typename T, typename U> struct next_up<T, U, 1, true> { typedef unsigned type; };
template <typename T, typename U> struct next_up<T, U, 2, false> { typedef int type; };
template <typename T, typename U> struct next_up<T, U, 2, true> { typedef unsigned type; };
template <typename T, typename U> struct next_up<T, U, 4, false> { typedef int64_t type; };
template <typename T, typename U> struct next_up<T, U, 4, true> { typedef uint64_t type; };
#ifdef __SIZEOF_INT128__
template <typename T, typename U> struct next_up<T, U, 8, false> { typedef __int128_t type; };
template <typename T, typename U> struct next_up<T, U, 8, true> { typedef __uint128_t type; };
#endif
/** Base template for a saturating integer or unsigned integer. */
template <typename T, typename TNOTUSED = typename std::enable_if<std::is_integral<T>::value, T>::type>
class xint_sat_t {
public:
typedef xint_sat_t<T> return_type; ///< This is what the operators return
/** Create a new zero-initialized saturated type. */
constexpr xint_sat_t() : value{0} {}
/**
* Create a new saturating type based on a given value.
* @param val Initial value will be clamped to fit T
*/
template <typename U>
constexpr xint_sat_t(const U& val) : value{clamp(val)} {}
/** Conversion back to the base type */
constexpr operator const T&() const { return value; }
constexpr operator T&() { return value; }
/**
* Add `other` to this value and return a new saturating type.
* @param other Value to add to this one
* @return New saturating type
*/
template <typename U>
constexpr return_type __attribute__((pure)) add(const U& other) const {
if constexpr (std::is_unsigned<T>::value) {
if constexpr (std::is_unsigned<U>::value) {
const auto temp = (typename next_up<T, U>::type)value + other;
return {
temp > std::numeric_limits<T>::max()
? std::numeric_limits<T>::max()
: (T)temp
};
// Branchless version, seems to compile down to exactly the same thing in GCC
// auto temp = value + other;
// temp |= -(temp < value);
// return { temp };
// Slower:
// const auto temp = value + (T)other;
// return { (temp < value) ? std::numeric_limits<T>::max() : temp };
} else {
if (other < 0) {
if constexpr (sizeof(U) > 4) {
const uint64_t temp = -other;
return {
(value > temp) ? (T)(value - temp) : 0
};
} else {
const unsigned temp = -other;
return {
(value > temp) ? (T)(value - temp) : 0
};
}
} else {
const auto temp = (typename next_up<T, U>::type)value + other;
return {
temp > std::numeric_limits<T>::max()
? std::numeric_limits<T>::max()
: (T)temp
};
}
}
} else {
const auto temp = (typename next_up<T, U>::type)value + other;
return {
temp > std::numeric_limits<T>::max()
? std::numeric_limits<T>::max()
: (temp < std::numeric_limits<T>::min()
? std::numeric_limits<T>::min()
: (T)temp)
};
}
}
/**
* Substract `other` from this value and return a new saturating type.
* @param other Value to substract from this one
* @return New saturating type
*/
template <typename U>
constexpr return_type __attribute__((pure)) substract(const U& other) const {
if constexpr (std::is_unsigned<T>::value) {
if constexpr (std::is_unsigned<U>::value) {
return {
other > value
? 0
: (T)(value - other)
};
} else {
if (other < 0) {
const auto temp = (typename next_up<T, U>::type)(-other) + value;
return {
temp > std::numeric_limits<T>::max()
? std::numeric_limits<T>::max()
: (T)temp
};
} else {
return {
value > other
? (T)(value - other)
: 0
};
}
}
} else {
const auto temp = (typename next_up<T, U>::type)value - other;
return {
temp > std::numeric_limits<T>::max()
? std::numeric_limits<T>::max()
: (temp < std::numeric_limits<T>::min()
? std::numeric_limits<T>::min()
: (T)temp)
};
}
}
/**
* Multiply this value with `other` and return a new saturating type.
* @param other Multiplication factor
* @return New saturating type
*/
template <typename U>
constexpr return_type __attribute__((pure)) multiply(const U& other) const {
if constexpr (std::is_unsigned<T>::value) {
if constexpr (std::is_unsigned<U>::value) {
const auto temp = (typename next_up<T, U>::type)value * other;
return {
temp > std::numeric_limits<T>::max()
? std::numeric_limits<T>::max()
: (T)temp
};
} else {
if (other < 0) {
return 0;
} else {
const auto temp = (typename next_up<T, U>::type)value * other;
return {
temp > std::numeric_limits<T>::max()
? std::numeric_limits<T>::max()
: (T)temp
};
}
}
} else {
const auto temp = (typename next_up<T, U>::type)value * other;
return {
temp > std::numeric_limits<T>::max()
? std::numeric_limits<T>::max()
: (temp < std::numeric_limits<T>::min()
? std::numeric_limits<T>::min()
: (T)temp)
};
}
}
/**
* Divide this by `other` and return a new saturating type.
* @param other Division factor
* @return New saturating type
*/
template <typename U>
constexpr return_type __attribute__((pure)) divide(const U& other) const {
if (other == 0) {
return std::numeric_limits<T>::max();
} else {
return value / other;
}
}
constexpr auto& operator++() {
if (value < std::numeric_limits<T>::max() - 1) ++value;
return *this;
}
constexpr auto operator++(int) {
xint_sat_t<T> temp { value };
if (value < std::numeric_limits<T>::max() - 1) ++value;
return std::move(temp);
}
constexpr auto& operator--() {
if (value > std::numeric_limits<T>::min() + 1) --value;
return *this;
}
constexpr auto operator--(int) {
xint_sat_t<T> temp { value };
if (value > std::numeric_limits<T>::min() + 1) --value;
return std::move(temp);
}
template <typename U> constexpr auto& operator= (const U& other) { value = clamp(other); return *this; }
template <typename U> constexpr decltype(auto) __attribute__((pure)) operator+(const U& other) const { return add(other); }
template <typename U> constexpr decltype(auto) __attribute__((pure)) operator-(const U& other) const { return substract(other); }
template <typename U> constexpr decltype(auto) __attribute__((pure)) operator*(const U& other) const { return multiply(other); }
template <typename U> constexpr decltype(auto) __attribute__((pure)) operator/(const U& other) const { return divide(other); }
template <typename U> constexpr return_type __attribute__((pure)) operator%(const U& other) const { return value % other; }
template <typename U> constexpr auto& operator+=(const U& other) { value = add(other); return *this; }
template <typename U> constexpr auto& operator-=(const U& other) { value = substract(other); return *this; }
template <typename U> constexpr auto& operator*=(const U& other) { value = multiply(other); return *this; }
template <typename U> constexpr auto& operator/=(const U& other) { value = divide(other); return *this; }
template <typename U> constexpr auto& operator%=(const U& other) { value %= other; return *this; }
private:
T value;
template <typename U>
constexpr T clamp(const U& val) const {
if constexpr (std::is_unsigned<T>::value == std::is_unsigned<U>::value && sizeof(U) <= sizeof(T)) {
return val;
} else {
return (val < std::numeric_limits<T>::lowest())
? std::numeric_limits<T>::lowest()
: (val > std::numeric_limits<T>::max()
? std::numeric_limits<T>::max()
: val);
}
}
};
}
typedef xint_sat_t<int8_t> int_sat8_t;
typedef xint_sat_t<uint8_t> uint_sat8_t;
typedef xint_sat_t<int16_t> int_sat16_t;
typedef xint_sat_t<uint16_t> uint_sat16_t;
typedef xint_sat_t<int32_t> int_sat32_t;
typedef xint_sat_t<uint32_t> uint_sat32_t;
typedef xint_sat_t<int64_t> int_sat64_t;
typedef xint_sat_t<uint64_t> uint_sat64_t;
As promised below a link to the updated version: https://github.com/StefanHamminga/saturating
int_sat8_t a = -128; int_sat8_t b = -a;
? Usual 2s complement saysb
should be-128
, with "overflow protection" this could be127
instead (but that would require to overload the unary monus operator). \$\endgroup\$int_sat8_t b = -1 * (-128)
would (and does) result in127
. Would there be a mathematical reason not to expect this result? \$\endgroup\$int_sat8_t c = -b; assert(a == c);
(with the fix,a == -128
andc == -127
). That said, not fixing the unary minus operator would be inconsistent with multiplying by-1
, and that operation already has this behavior. That said, it makes multiplication non-cumulative ((-1) * (-1) * (-128) == 1 * (-128) == -128
,(-1) * (-128) * (-1) == 127 * (-1) == -127
). Such is the price for overflow protection. (Or one could simply make-128
an invalid value, kinda like NaN, sidestepping the problem). \$\endgroup\$