CNL  rev.0.1.0
Compositional Numeric Library
User Manual

Introduction

The Compositional Numeric Library (CNL) is a C++ library of fixed-precision numeric classes which enhance integers to deliver safer, simpler, cheaper arithmetic types.

CNL improves on numbers the same way that the STL improves on arrays and smart pointers improve on pointers.

Uses include

  • simulations that rely on precision, determinism, and predictability,
  • financial applications handling decimal fractions and
  • resource-critical systems where floating-point arithmetic is too costly.

The CNL project is published under the Boost licence and can be found online at github.com/johnmcfarlane/cnl.

Components

Core Components

The core numeric types of CNL are:

  • fixed_point - scales integers by any exponent and base with zero-overhead and minimal precision loss;
  • elastic_integer - prevents overflow at compile-time by generalizing promotion rules;
  • overflow_integer - handles integer overflow at run-time;
  • rounding_integer - improves rounding behavior of integers;
  • wide_integer - provides integers wider than 64 and 128 bits using multi-word arithmetic;
  • fraction - low-level dividend/divisor pair aids refined division handling and
  • constant - a numerics-friendly alternative to std::integral_constant.

Each of these types solves a single problem when used alone.

Composite Types

In combination, the core types produce composites which address multiple concerns.

Provided composite types include:

Many more combinations are possible. For example:

Examples

The following examples can be found in the test suite.

Declaration

The fixed_point type adds a scaling exponent to integers. This enables the integer to represent very big or very small values.

#include <cnl/all.h>
#include <iostream>
using namespace std;
void declaration_example()
{
// x is represented by an int and scaled down by 1 bit
auto x = fixed_point<int, -1>{3.5};
// under the hood, x stores a whole number
cout << to_rep(x) << endl; // "7"
// but it multiplies that whole number by 2^-1 to produce a real number
cout << x << endl; // "3.5"
// like an int, x has limited precision
x /= 2;
cout << x << endl; // "1.5"
}

Arithmetic Operators

Specializations of fixed_point behave a lot like native C/C++ numeric types. Operators are designed to behave in an way which is both predictable and efficient.

void basic_arithmetic_example()
{
// define a constant signed value with 3 integer and 28 fraction bits (s3:28)
auto pi = fixed_point<int32_t, -28>{3.1415926535};
// expressions involving integers return fixed_point results
auto tau = pi*2;
static_assert(is_same<decltype(tau), fixed_point<int32_t, -28>>::value, "");
// "6.28319"
cout << tau << endl;
// expressions involving floating-point values return floating-point results
auto degrees = tau*(180/3.1415926534);
static_assert(is_same<decltype(degrees), double>::value, "");
// "360"
cout << degrees << '\n';
}

Arithmetic Functions

But one size does not fit all. Different applications of the same operation might call for different trade-offs between storage, precision, safety and speed. Named functions provide fine-tuned control over arithmetic results.

void advanced_arithmetic_example()
{
// this variable uses all of its capacity
auto x = fixed_point<uint8_t, -4>{15.9375};
// 15.9375 * 15.9375 = 254.00390625 ... overflow!
auto xx1 = fixed_point<uint8_t, -4>{x*x};
cout << xx1 << endl; // "14" instead!
// fixed-point multiplication operator obeys usual promotion and implicit conversion rules
auto xx = x*x;
// x*x is promoted to fixed_point<int, -8>
static_assert(is_same<decltype(xx), fixed_point<int, -8>>::value, "");
cout << xx << endl; // "254.00390625" - correct
// you can avoid the pitfalls of integer promotion for good by using the elastic_number type
auto named_xx = make_elastic_number(x) * make_elastic_number(x);
// this type tracks both the number of digits and the exponent to ensure lossless multiplication
static_assert(is_same<decltype(named_xx), elastic_number<16, -8, unsigned>>::value, "");
cout << named_xx << endl; // "254.00390625" - also correct but prone to overflow
}

Extensible

Because one size does not fit all, fixed_point is designed to make it easy to tailor new arithmetic types. The elastic_number type illustrates this. As each calculation requires more digits, so the results of elastic_number operations allocate more storage.

void elastic_example1()
{
// Consider an integer type which keeps count of the bits that it uses.
auto a = elastic_integer<6, int8_t>{ 63 };
// Results of its operations widen as required.
auto aa = a*a;
static_assert(is_same<decltype(aa), elastic_integer<12, int8_t >> ::value, "");
// Obviously, this type no longer fits in a byte.
static_assert(sizeof(aa)==2, "");
// Addition requires smaller results.
auto a2 = a+a;
static_assert(is_same<decltype(a2), elastic_integer<7, int8_t >> ::value, "");
}
void elastic_example2()
{
// A type such as elastic_integer can be used to specialize fixed_point.
// Now arithmetic operations are more efficient and less error-prone.
auto b = elastic_number<31, -27, unsigned>{15.9375};
auto bb = b*b;
cout << bb << endl; // "254.00390625"
static_assert(is_same<decltype(bb), elastic_number<62, -54, unsigned>>::value, "");
}