![]() |
Safe Numerics |
Our system works by checking arithmetic operations whenever they could result in an erroneous result. The C++ standard describes how binary operations on different integer types are handled. Here is a simplified version of the rules:
promote any operand smaller than int to an int or unsigned int.
if the signed operand is larger than the signed one, the result will be signed, otherwise the result will be unsigned.
expand the smaller operand to the size of the larger one
So the result of the sum of two integer types will result in another integer type. If the values are large, they will exceed the size that the resulting integer type can hold. This is what we call "overflow". Standard C++ just truncates the result to fit into the result type - which makes the result arithmetically incorrect. Up until now, we've focused on detecting when this happens and invoking an interrupt or other kind of error handler. But now we look at another option. Using the "automatic" type promotion policy, we can change the rules of C++ arithmetic for safe types to something like the following:
for any C++ numeric types, we know from
std::numeric::limits what the maximum and minimum
values that a variable can be - this defines a closed
interval.
For any binary operation on these types, we can calculate the interval of the result.
From this we can determine a new safe type which can be guarenteed to hold the result.
Since the result type is guarenteed to hold the result, there is no need to check for errors - they can't happen !!!
The only error checking we need to do is when safe values are initialized, but this we would have to do in any case. So we've eliminated arithmetically incorrect results while incurring zero runtime overhead for error checking.
In sort, given a binary operation, we promote the constituent types to a larger result type which can't overflow. This is a fundamental deparature from the C++ Standard behavior.
If the interval of the result cannot be contained in the largest type that the machine can handle (usually 64 bits these days), the largest available integer type with the correct result sign is used. So even with our "automatic" type promotion scheme, it's still possible to overflow. In this case, and only this case, is runtime error checking code generated. Depending on the application, it should be rare to generate error checking code, and even more rare to actually invoke it.
This small example illustrates how to use type promotion and how it works.
#include <cassert>
#include <stdexcept>
#include <ostream>
#include <iostream>
#include <cxxabi.h>
#include "../include/safe_range.hpp"
#include "../include/automatic.hpp"
template <
std::intmax_t Min,
std::intmax_t Max
>
using safe_t = boost::numeric::safe_signed_range<
Min,
Max,
boost::numeric::automatic,
boost::numeric::throw_exception
>;
// I can't figure out how to overload os << for safe_t
// we use the following workaround there
// wrap a safe_t in a "formatted" wrapper
template<typename T>
struct formatted {
using wrapped_type = T;
const T & m_t;
formatted(const T & t) :
m_t(t)
{}
};
template<typename T>
auto make_formatted(const T & t){
return formatted<T>(t);
}
// now (fully) specialize output of safe types wrapped in formatted
template<
class T,
T Min,
T Max,
class P, // promotion polic
class E // exception policy
>
std::ostream & operator<<(
std::ostream & os,
const formatted<boost::numeric::safe_base<T, Min, Max, P, E>> & f
){
using safe_type = typename formatted<boost::numeric::safe_base<T, Min, Max, P, E> >::wrapped_type;
return os
<< "["
<< std::numeric_limits<safe_type>::min() << ","
<< std::numeric_limits<safe_type>::max() << "] = "
<< f.m_t;
}
int main(int argc, const char * argv[]){
// problem: checking of externally produced value can be overlooked
std::cout << "example 8: ";
std::cout << "eliminate runtime overhead"
<< std::endl;
try{
int status;
const safe_t<-64, 63> x(1);
std::cout << abi::__cxa_demangle(typeid(x).name(),0,0,&status) << '\n';
std::cout << "x" << make_formatted(x) << std::endl;
safe_t<-64, 63> y;
y = 2;
std::cout << "y" << make_formatted(y) << std::endl;
auto z = x + y;
std::cout << "(x + y)" << make_formatted(z) << std::endl;
std::cout << "(x - y)" << make_formatted(x - y) << std::endl;
}
catch(std::exception e){
// none of the above should trap. Mark failure if they do
std::cout << e.what() << std::endl;
return false;
}
return 0;
}