...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
Safe Numerics |
When trying to avoid arithmetic errors of the above type,
programmers will select data types which are wide enough to hold values
large enough to be certain that results won't overflow, but are not so
large as to make the program needlessly inefficient. In the example below,
we presume we know that the values we want to work with fall in the range
[-24,82]. So we "know" the program will always result in a correct result.
But since we trust no one, and since the program could change and the
expressions be replaced with other ones, we'll still use the loose_trap_policy
exception policy to verify at compile time that what we "know" to be true
is in fact true.
#include <iostream> #include <boost/safe_numerics/safe_integer_range.hpp> #include <boost/safe_numerics/safe_integer_literal.hpp> #include <boost/safe_numerics/exception.hpp> #include <boost/safe_numerics/native.hpp> #include "safe_format.hpp" // prints out range and value of any type using namespace boost::safe_numerics; // create a type for holding small integers in a specific range using safe_t = safe_signed_range< -24, 82, native, // C++ type promotion rules work OK for this example loose_trap_policy // catch problems at compile time >; // create a type to hold one specific value template<int I> using const_safe_t = safe_signed_literal<I, native, loose_trap_policy>; // We "know" that C++ type promotion rules will work such that // addition will never overflow. If we change the program to break this, // the usage of the loose_trap_policy promotion policy will prevent compilation. int main(int, const char *[]){ std::cout << "example 83:\n"; constexpr const const_safe_t<10> x; std::cout << "x = " << safe_format(x) << std::endl; constexpr const const_safe_t<67> y; std::cout << "y = " << safe_format(y) << std::endl; const safe_t z = x + y; std::cout << "x + y = " << safe_format(x + y) << std::endl; std::cout << "z = " << safe_format(z) << std::endl; return 0; }
defines a type
which is limited to the indicated range. Out of range assignments
will be detected at compile time if possible (as in this case) or at
run time if necessary.safe_signed_range
A safe range could be defined with the same minimum and
maximum value effectively restricting the type to holding one
specific value. This is what safe_signed_literal
does.
Defining constants with safe_signed_literal
enables the library to correctly anticipate the correct range of the
results of arithmetic expressions at compile time.
The usage of
will mean that any assignment to z which could be outside its legal
range will result in a compile time error.loose_trap_policy
All safe integer operations are implemented as constant
expressions. The usage of constexpr
will guarantee that
z
will be available at compile time for any subsequent
use.
So if this program compiles, it's guaranteed to return a valid result.
The output uses a custom output manipulator,
safe_format
, for safe types to display the underlying type
and its range as well as current value. This program produces the
following run time output.
example 83: x = <signed char>[10,10] = 10 y = <signed char>[67,67] = 67 x + y = <int>[77,77] = 77 z = <signed char>[-24,82] = 77
Take note of the various variable types:
x
and y
are safe types with fixed
ranges which encompass one single value. They can hold only that
value which they have been assigned at compile time.
The sum x + y can also be determined at compile
time.
The type of z is defined so that It can hold only values in
the closed range -24,82. We can assign the sum of x + y because it
is in the range that z
is guaranteed to hold. If the
sum could not be be guaranteed to fall in the range of
z
, we would get a compile time error due to the fact we
are using the loose_trap_policy
exception
policy.
All this information regarding the range and values of
variables has been determined at compile time. There is no runtime
overhead. The usage of safe types does not alter the calculations or
results in anyway. So safe_t
and const_safe_t
could be redefined to int
and const int
respectively and the program would operate identically - although it might
We could compile the program for another machine - as is common when
building embedded systems and know (assuming the target machine
architecture was the same as our native one) that no erroneous results
would ever be produced.