Bbi
Better Behaved Integers
Install / Use
/learn @HowardHinnant/BbiREADME
Better Behaved Integers (bbi)
Overview
The purpose of this header-only C++20 library is to provide integral arithmetic with greatly reduced undefined behavior. Additionally some of the more irritating behavior of C and C++ integers is made a little more civilized.
For example, there is no integer promotion—that is, the sum of two 8 bit integers is an 8 bit integer—and all implicit conversions are value-preserving. Explicit conversions are supplied for conversions that are not value-preserving, and comparisons between signed and unsigned types always give the right answer. Finally, if you need an integer with twice the bit width of the one you're using, this library provides it.
Example use:
#include "bbi.h"
#include <iostream>
int
main()
{
// This exposes names of integers such as i32 (signed 32 bit integral type)
using namespace bbi::wrap;
i32 x = 2;
i32 y = 3;
i32 z = x + y;
std::cout << x << " + " << y << " = " << z << '\n';
}
Output:
2 + 3 = 5
So, nothing special, right?
For the most part, right! This library should dissolve into the background and become nearly invisible. However, there are corner cases where the C++ language can wreak havoc on your code when dealing with integral types. The rest of this document seeks to address that problem by demonstrating what damage can be done, and how this library intends to diagnose and potentially fix it.
Modes
There are 5 behavioral modes which determine what happens on overflow: wrap,
sat, thrw, term, and raw. They are most easily accessed with a using
directive, for example:
using namespace bbi::wrap;
When this is issued, the following types are available to model signed and unsigned integers with the indicated bit width:
i8
i16
i32
i64
i128
i256
i512
i1024
i2048
i4096
u8
u16
u32
u64
u128
u256
u512
u1024
u2048
u4096
Each of these are a type alias to a specialization of
template <SignTag S, unsigned N, Policy P> class bbi::Z;
Where SignTag can be bbi::Signed or bbi::Unsigned, N can be any power of
2 that is greater than or equal to 8 (bounded by your compiler's memory limits,
and your patience for performance), and Policy is one of bbi::Wrap,
bbi::Saturate, bbi::Terminate, or bbi::Throw.
Each Policy behaves just like the built-in integral types, except that there is no integral promotion and there is no undefined behavior when overflow occurs: the policy dictates what the overflow behavior should be.
wrap
This is modulo 2<sup>n</sup> arithmetic where n is the bit width. This is also known as two's complement. The difference between this and C++'s arithmetic is that signed overflow is well-defined.
Example:
#include "bbi.h"
#include <iostream>
int
main()
{
using namespace bbi::wrap;
i8 x{100};
std::cout << x*x << '\n';
}
Output:
16
Note: i8 is a numeric type, not a character type. This is 100*100 computed
with infinite precision but then truncated back into 8 bits (10,000 % 128).
Note: Division by zero, and mod 0 when using wrap remains undefined behavior.
However in other modes this behavior is defined (to saturate, throw or terminate).
sat
On overflow saturate to either the minimum or maximum representable value, whichever is the closest to the exact answer.
Example:
#include "bbi.h"
#include <iostream>
int
main()
{
using namespace bbi::sat;
i8 x{100};
std::cout << x*x << '\n';
}
Output:
127
Note: This is 100*100 computed with infinite precision but then truncated back
to the closet representable value in i8.
Note: Division by zero saturates to either the minimum or maximum value,
depending on sign. Mod (operator%) by zero is an expensive no-op in this
mode. The modulus x % y is computed by x - x/y*y. When y is 0 the
second term is 0, and the result is x.
thrw
On overflow throw a std::overflow_error.
Example:
#include "bbi.h"
#include <iostream>
int
main()
{
using namespace bbi::thrw;
i8 x{100};
std::cout << x*x << '\n';
}
Output:
terminating due to uncaught exception of type std::overflow_error:
Z<Signed, 8, Throw>(100) * Z<Signed, 8, Throw>(100) overflowed
Note: i8 is a type alias for Z<Signed, 8, Throw>.
term
On overflow std::terminate after an informative message to std::cerr.
Example:
#include "bbi.h"
#include <iostream>
int
main()
{
using namespace bbi::term;
i8 x{100};
std::cout << x*x << '\n';
}
Output:
Z<Signed, 8, Terminate>{100) * Z<Signed, 8, Terminate>{100) overflowed
libc++abi: terminating
raw
The bbi integral types alias the corresponding standard integral types such as
std::int8_t. This allows one to easily switch between using this library for
debugging and testing, and using the standard integral types for release code.
The following types don't exist in this namespace: i128, i256, i512, i1024,
u128, u256, u512, and u1024.
Example:
#include "bbi.h"
#include <iostream>
int
main()
{
using namespace bbi::raw;
i8 x{100};
std::cout << x*x << '\n';
}
Output:
10000
Note: Integral promotion to int occurred. Note that in the other modes,
no integral promotion occurred.
Arithmetic Summary
The above examples all used the same type for both operands. When the operands
are of different types, but have the same Policy, they can be operated on as if both
operands are first brought to a common type using std::common_type.
Unary negation
In wrap mode this operation will negate and wrap on overflow. If the type is
signed, negating the minimum value results in the same value (without undefined behavior).
In sat mode this operation will negate as if a signed infinite precision type and then
truncate to the minimum or maximum if the result is out of range. For example if the
type is unsigned, the result is always 0. And if the type is signed and the value is
the minimum, the result is the maximum.
In the thrw and term modes this operation will negate as if a signed infinite precision type and then
if the result is out of range, throw or terminate respectively. For example, if the type
is unsigned, negating any value but 0 will throw or terminate. And if the type is
signed, negating the minimum value will throw or terminate.
Conversions
Explicit conversions
All bbi integral types (signed and unsigned, any bit width, and any Policy) can
be explicitly converted to one another, and to/from C++ integral types.
However, if the destination has a Throw or Terminate Policy, and if the run-time value
of the source can not be preserved, then an exception is thrown or terminate is called.
Example:
#include "bbi.h"
#include <iostream>
int
main()
{
using namespace bbi::wrap;
i64 x = 0x7FFF'FFFF'FFFF'FFFF;
i32 y{x};
std::cout << y << '\n';
}
Output:
-1
Example:
#include "bbi.h"
#include <iostream>
int
main()
{
using namespace bbi::thrw;
i64 x = 0x7FFF'FFFF'FFFF'FFFF;
i32 y{x};
std::cout << y << '\n';
}
Output:
terminating due to uncaught exception of type std::overflow_error:
Z<Signed, 32, Throw>(Z<Signed, 64, Throw>(9223372036854775807)) overflowed
An explicit conversion from std::string_view to a bbi integral type is allowed,
as well as an explicit conversion from a bbi integral type to std::string. This
facilitates I/O.
Example:
#include "bbi.h"
#include <iostream>
#include <string>
#include <string_view>
int
main()
{
using namespace bbi::term;
i8 i{"127"};
std::cout << i << " as string: " << std::string{i} << '\n';
i8 j{"128"};
}
Output:
127 as string: 127
Z<Signed, 8, Terminate>{120} + Z<Signed, 8, Terminate>{8} overflowed
libc++abi: terminating
Implicit conversions
Type bbi::X can be implicitly converted to type bbi::Y if all representable
values of X can be represented by Y, and if X and Y have the same
Policy.
Example:
i8 i{};
u32 u = i;
Output:
error: no viable conversion from 'Z<bbi::Signed, 8, [...]>' to 'Z<bbi::Unsigned, 32, [...]>'
u32 u = i;
^ ~
Note: Even though u has lots more bits than i, u can't represent
negative values that i may have. Therefore an implicit conversion is not
allowed.
Corollary: Implicit conversions from C++ standard integral types to bbi integral types
are allowed only if the bbi integral type can represent all values of the standard
integral type. Note, the set of standard integral types excludes bool and character
types. signed char and unsigned char are standard integral types, not character
types. For those built-in integral types that are not standard integral types (bool
and character types) only explicit conversion is allowed.
Example:
i8 x = 0; // compile-time error: 0 has type int that could hold values that don't fit in 8 bits
i8 y = std::int8_t{0}; // ok
std::common_type
When two or more bbi integers have the same Policy, they are fully integrated
into the std::common_type infrastructure. Given two such integers X and Y,
std::common_type_t<X, Y> exists, and both X and Y are implicitly
convertible to std::common_type_t<X, Y>. From this it follows that
std::common_type_t<X, Y> can represent the union of the values representable
by X and Y. If X or Y (but not both) is a standard integral type, then
the result is the same as if the integral type is converted to a bbi integral
type with the same sign, bit width as integral type, and the same policy as the
bbi integral type, and then the common_type is found. If X and Y are bbi
types with different Policies, there is no common_type, and if attempted, a
compile-time error will result.
Examples:
| X | Y | `common_typ
