Table of Contents
Arithmetic operations in C++ are NOT guaranteed to yield a correct mathematical result. For instance, the overflow on addition operation may produce an infinite value. Safe Float proposes implementing a drop-in replacement for floating point numeric data types guaranteeing that, when used in expressions in a C++ program, no unexpected arithmetic results will be produced silently without the developer awareness.
An example of the problem:
#include <iostream> #include <limits> int main(){ using namespace std; float a = numeric_limits<float>::max(); float b = a + numeric_limits<float>::max(); cout << "b is " << b << endl; return 0; }
Executing this code outputs:
b is inf
This is a convenient fallback for the overflow in some contexts, but an unexpected silent surprise in others. Overflow to infinite is not the only surprise silently introduced by floating point operations, neither the most dangerous. it is actually the one most programmers care to check.
In 1991, David Goldberg collected a list of the most common dangers when developing using Floating Point, which can be found on his paper: "What every computer scientist should know about floating-point arithmetic".
Most important dangers can be classified as follows:
Rounding error
Some operations may require rounding the result. (i.e. because the mantissa is not large enough to keep the result.)
Definition error. (i.e. 0.3 is not expressible as Floating Point.)
An interesting side-effect of rounding is that operations are not guaranteed to be reversible: a != a+b-b is a valid result.
Not a Number
Some operations may produce Not a Number results, and they are not detected until used (if signalling is on).
Infinity by overflow
Infinity arithmetic is useful mostly when infinity values are defined by user, and not because of an overflow.
Signed zero
In some cases as discontinuous functions the negative zero have important use.
Underflow (considering and not considering normalised numbers as underflow)
A real world example of the issue is the use of Floating Point in Discrete Event System Specification simulation to define the temporal position of the events in a Real timeline. In this context, developers intensively use Floating Point and they usually give a special meaning to the infinite values called Passive state in the literature. In their context, an infinite obtained as overflow operating with finite numbers produces incorrect simulation result, because it silently changes the state to passive when it should not.
In C++11 standard cfenv.h was introduced facilitating the access to the Floating Point Unit flags for controlling and checking information related to Floating Point operations. This allows us to have a higher level of control than the one we had on traditional C++.
Anyway, developer intervention is required for checking every Floating Point operation was conducted as expected. Doing so, renders the code harder to read, maintain and it adds a new source of potential programming errors.
Safe_float proposes the introduction of a class template wrapping Floating Point data types introducing safety checks for arithmetic operations. In the case an operation result is unreliable it reports the user properly.
Depending on the algorithms implemented, different safety measures may be required, in our previously described example with the use of infinities for Discrete Event Simulation, the algorithms are not sensitive to rounding and this should not be treated as a safety problem, neither have a performance penalty based the unused check.
Since, the reliability of the results is based on the algorithms used, we propose letting developers declare their concerns using template parameters as policies.
We try to provide policies for most of the dangers before mentioned. However, some of the checks may be missing. To see the full list of the checks implemented by safe_float please check the Policies section of the documentation.
We show some usage examples below for illustration.
//no underflow example safe_numerics<double, no_underflow, throw_on_fail> a = 1; safe_numerics<double, no_underflow, throw_on_fail> b = numeric_limits<double>::max(); safe_numerics<double, no_underflow, throw_on_fail> c = a / b ; //throws because of underflow. //no rounding example safe_numerics<double, no_rounding, abort_on_fail> d = 0.3; // compilation error, the number is not representable. safe_numerics<double, no_rounding, abort_on_fail> e = 2E25; safe_numerics<double, no_rounding, abort_on_fail> f = 2E100; safe_numerics<double, no_rounding, abort_on_fail> g = e + f ; //aborts execution with runtime error. //multiple conditions example safe_numerics<double, BSF_COMBINE(no_rounding, no_overflow_to_infinity)> h = numeric_limits<double>::max(); safe_numerics<double, BSF_COMBINE(no_rounding, no_overflow_to_infinity)> i = numeric_limits<double>::max(); safe_numerics<double, BSF_COMBINE(no_rounding, no_overflow_to_infinity)> j = h + i; // throws because of overflow to infinity.
Provides a drop-in replacement for native Floating Point data types producing one of the following outcomes for each operation:
A valid result covering the user safety definition.
A compilation error.
A (customized) report of the safety condition was not covered, for example by throwing an exception or loging the violation.
The implementation is based in user defined policies that encapsulate what to check and how to how to inform when check fails.
Current implementation apply the policies to the native Floating Point types: float, double, long double. The checks are limited to arithmetic operators and numeric_limits is specialized safe_float
Extending to support Boost.Multiprecission is planned as future work. Also, we plan for future work adding support for Boost.Math and std::math.
safe_float is a headers-only library with few requirements:
A compiler supporting C++17 standard.
A compiler supporting pragma FENV is not required.
For building examples and tests B2 is used.
Alternatively support for cmake is also offered.
Some examples may have require extra dependencies from Boost, including Boost.ProgramOptions, but these dependencies are not necessary for using the library.
Tests are implemented using Boost.Test compiled version and they require also Boost.MPL.
B2 scripts detect if current compiler has support for FENV pragma, however, it is not detected in cmake builts, neither it is used for optimizations.
FENV enabled implementation was never tested since g++ and clang++ does not support it yet.
The exception strong guarantee may have been broken in some places, this is a temporary problem.
The throw_on_error reporter is throwing always boost::safe_float_exception, subclasses of it might be used in the future to provide finer grain catching.
Last revised: June , 2018 |