checkedint

Checked integer arithmetic operations, functions, and types with improved handling of errors and corner cases compared to the basic integral types.

Note: Normally this module should not be imported directly. Instead, import one of checkedint.throws, checkedint.asserts, or checkedint.sticky, depending on which error signalling policy is needed. (See below.)

$(BIG $(B Problems solved by `checkedint`))
As in many other programming languages (C, C++, Java, etc.) D's basic integral types (such as int or ulong) are surprisingly difficult to use correctly in the general case, due to variuos departures from the behaviour of ideal mathematical integers:

  • Overflow silently wraps around: assert(uint.max + 1 == 0);
  • Mixed signed/unsigned comparisons often give the wrong result: assert(-1 > 1u);
  • Mixed signed/unsigned arithmetic operations can also give the wrong result.
  • Integer division by zero crashes the program with a mis-named and uncatchable Floating Point Exception (FPE).
  • int.min / -1 and int.min % -1 may also crash with an FPE, even though the latter should simply yield 0.
  • If x is any integer value, and y is any negative integer value, x ^^ y will crash with an FPE.
  • No bounds checking is done when casting from one integer type to another.
  • The result of the bitshift operations (<<, >>, >>>) is formally undefined if the shift size is less than zero or greater than (8 * N.sizeof) - 1.

The checkedint package offers solutions to all of these issues and more.

$(BIG $(B `SafeInt` versus `SmartInt`))
Two different approaches are available:

  • SmartInt and smartOp strive to actually give the mathematically correct answer whenever possible, rather than just signaling an error.
  • SafeInt and safeOp strive to match the behaviour of the basic integral types exactly, except that where the behaviour of the basic type is wrong, or very unintuitive, an error is signaled instead.

There is no meaningful performance difference between SafeInt and SmartInt. For general use, choosing SmartInt simplifies code and maximizes the range of inputs accepted.

SafeInt is intended mainly as a debugging tool, to help identify problems in code that must also work correctly with the basic integral types. The DebugInt template alias makes it simple to use of SafeInt in debug builds, and raw basic types in release builds.

int (basic type)SafeInt!intSmartInt!int
int.max + 1int.minraise(IntFlag.over)raise(IntFlag.over)
-1 > 1utruecompile-time errorfalse
-1 - 2u4294967293compile-time error-3
1 / 0crash by FPEraise(IntFlag.div0)raise(IntFlag.div0)
int.min % -1crash by FPEraise(IntFlag.posOver)0
-1 ^^ -7crash by FPEraise(IntFlag.undef)-1
cast(uint)-14294967295compile-time errorraise(IntFlag.negOver)
-1 >> 100undefinedraise(IntFlag.undef)-1

$(BIG $(B Error Signaling))
Some types of problem are signaled by a compile-time error, others at runtime. Runtime signaling is done through checkedint.flags. Three different runtime signalling policies are available:

  • With IntFlagPolicy.throws, a CheckedIntException is thrown. These are normal exceptions; not FPEs. As such, they can be caught and include a stack trace.
  • With IntFlagPolicy.asserts, an assertion failure will be triggered. This policy is compatible with pure nothrow @nogc code, but will crash the program in the event of a runtime integer math error.
  • Alternatively, IntFlagPolicy.sticky can be selected so that a thread-local sticky flag is set when an operation fails. This allows checkedint to be used from nothrow and @nogc (but not pure) code without crashing the program, but requires the API user to manually insert checks of IntFlags.local.

In normal code, there is no performance penalty for allowing checkedint to throw. Doing so is highly recommended because this makes it easier to use correctly, and yields more precise error messages when something goes wrong.

$(BIG $(B Generic Code))
The checkedint.traits module provides checkedint-aware versions of various numerical type traits from std.traits, such as Signed, isSigned and isIntegral. This allows writing generic algorithms that work with any of SmartInt, SafeInt, and the built-in numeric types such as uint and long.

Also of note is the idx() function, which concisely and safely casts from any integral type (built-in, SmartInt, or SafeInt) to either size_t or ptrdiff_t for easy array indexing.

$(BIG $(B Performance))
Replacing all basic integer types with SmartInt or SafeInt will slow down exectuion somewhat. How much depends on many factors, but for most code following a few simple rules should keep the penalty low:

  1. Build with -inline and -O (DMD) or -O3 (GDC and LDC). This by itself can improve the performance of checkedint by around 1,000%.
  2. With GDC or LDC, the performance hit in code that is bottlenecked by integer math will probably be between 30% and 100%. The performance hit may be considerably larger with DMD, due to the weakness of the inliner.
  3. checkedint can't slow down code where it's not used! For more speed, switch to DebugInt for the hottest code in the program (like inner loops) before giving up on checkedint entirely.

The above guidelines should be more than sufficient for most programs. But, some micro-optimization are possible as well, if needed:

  • Always use mulPow2(), divPow2(), and modPow2() whenever they can naturally express the intent - they're faster than a regular /, %, or pow().
  • Unsigned types are a little bit faster than signed types, assuming negative values aren't needed.
  • Although they are perfectly safe with checkedint, mixed signed/unsigned operations are a little bit slower than same-signedness ones.
  • The assignment operators (++ or +=, for example) should never be slower than the equivalent two operation sequence, and are sometimes a little bit faster.

References: core._checkedint

Modules

asserts
module checkedint.asserts

Aliases for the checkedint module using IntFlagPolicy.asserts.

flags
module checkedint.flags

Common error signaling facilities for the checkedint package.

sticky
module checkedint.sticky

Aliases for the checkedint module using IntFlagPolicy.sticky.

tests
module checkedint.tests

Test the correctness and performance of the checkedint package.

throws
module checkedint.throws

Aliases for the checkedint module using IntFlagPolicy.throws.

traits
module checkedint.traits

Templates to facilitate treating checkedint.SmartInt and checkedint.SafeInt like the built-in numeric types in generic code.

Members

Functions

safeInt
SafeInt!(N, policy, bitOps) safeInt(N num)

Get the value of num as a SafeInt!N. The integral type N can be infered from the argument.

smartInt
SmartInt!(N, policy, bitOps) smartInt(N num)

Get the value of num as a SmartInt!N. The integral type N can be infered from the argument.

Properties

bits
inout(N) bits [@property getter]
inout(N) bits [@property getter]
N bits [@property getter]
SmartInt!(BasicScalar!N, N.policy, Yes.bitOps) bits [@property getter]
SafeInt!(BasicScalar!N, N.policy, Yes.bitOps) bits [@property getter]

Get a view or copy of num that supports bitwise operations.

bscal
inout(N) bscal [@property getter]
inout(N) bscal [@property getter]
N bscal [@property getter]
BasicScalar!N bscal [@property getter]

Get a view or copy of num as a basic scalar.

idx
Select!(isSigned!N, ptrdiff_t, size_t) idx [@property getter]
Select!(isSigned!(BasicScalar!N), ptrdiff_t, size_t) idx [@property getter]

Cast num to a basic type suitable for indexing an array.

Structs

SafeInt
struct SafeInt(N, IntFlagPolicy _policy, Flag!"bitOps" bitOps = Yes.bitOps)

Wrapper for any basic integral type N that uses the checked operations from safeOp and rejects attempts to directly assign values that cannot be proven to be within the range representable by N. (checkedint.to() can be used to safely assign values of incompatible types, with runtime bounds checking.)

SmartInt
struct SmartInt(N, IntFlagPolicy _policy, Flag!"bitOps" bitOps = Yes.bitOps)

Wrapper for any basic integral type N that uses the checked operations from smartOp and bounds checks assignments with checkedint.to().

Templates

BasicScalar
template BasicScalar(T)

Aliases to the basic scalar type associated with T, assuming either:

  • isScalarType!T, or
  • isCheckedInt!T

Otherwise, BasicScalar aliases to void.

DebugInt
template DebugInt(N, IntFlagPolicy policy, Flag!"bitOps" bitOps = Yes.bitOps)

template alias that evaluates to SafeInt!(N, policy, bitOps) in debug mode, and N in release mode. This way, SafeInt!N is used to debug integer logic while testing, but the basic N is used in release mode for maximum speed and the smallest binaries.

SafeInt
template SafeInt(N, IntFlagPolicy policy, Flag!"bitOps" bitOps = Yes.bitOps)

Wrapper for any basic integral type N that uses the checked operations from safeOp and rejects attempts to directly assign values that cannot be proven to be within the range representable by N. (checkedint.to() can be used to safely assign values of incompatible types, with runtime bounds checking.)

SmartInt
template SmartInt(N, IntFlagPolicy policy, Flag!"bitOps" bitOps = Yes.bitOps)

Wrapper for any basic integral type N that uses the checked operations from smartOp and bounds checks assignments with checkedint.to().

hasBitOps
template hasBitOps(T)

Evaluates to true if either:

  • isScalarType!T, or
  • isCheckedInt!T

And bitwise operators such as << and ~ are available for T.

safeOp
template safeOp(IntFlagPolicy policy)

Implements various integer math operations with error checking.

smartOp
template smartOp(IntFlagPolicy policy)

Implements various integer math operations with error checking.

to
template to(T, IntFlagPolicy policy)

A wrapper for std.conv.to() which uses checkedint.flags for error signaling when converting between any combination of basic scalar types and checkedint types. With an appropriate policy, this allows checkedint.to() to be used for numeric conversions in pure nothrow code, unlike std.conv.to().

Meta

Authors

Thomas Stuart Bockman