![]() |
Safe Numerics |
Programming by Contract is a highly regarded technique. There has been much written about it has been proposed as an addition to the C++ language [Garcia][Crowl & Ottosen]. It (mostly depends upon runtime checking of parameter values upon entry to every function and thus slows the program down. This undermines the main motivation for using C++ in the first place! One popular scheme for addressing this issue is to enable parameter checking only during debugging and testing. This defeats the guarentee of correctness which we are seeking here! This library has facilities which, in many cases, can check guarentee parameter requirements with no runtime overhead. Consider the following example:
#include <cassert>
#include <stdexcept>
#include <sstream>
#include <iostream>
#include "../include/safe_range.hpp"
// return total number of minutes
unsigned int convert(
const unsigned int & hours,
const unsigned int & minutes
) {
// check that parameters are within required limits
// invokes a runtime cost EVERYTIME the function is called
// and the overhead of supporting an interrupt.
// note high runtime cost!
if(minutes > 59)
throw std::domain_error("minutes exceeded 59");
if(hours > 23)
throw std::domain_error("hours exceeded 23");
return hours * 60 + minutes;
}
// define convient typenames for hours and minutes hh:mm
using hours_t = boost::numeric::safe_unsigned_range<0, 23>;
using minutes_t = boost::numeric::safe_unsigned_range<0, 59>;
// return total number of minutes
// type returned is safe_unsigned_range<0, 24*60 - 1>
auto safe_convert(const hours_t & hours, const minutes_t & minutes) {
// no need for checking as parameters are guaranteed to be within limits
// expression below cannot throw ! zero runtime overhead
return hours * 60 + minutes;
}
int main(int argc, const char * argv[]){
std::cout << "example 7: ";
std::cout << "enforce contracts with zero runtime cost" << std::endl;
std::cout << "Not using safe numerics" << std::endl;
// problem: checking of externally produced value can be expensive
try {
convert(10, 83); // invalid parameters - detected - but at a heavy cost
}
catch(std::exception e){
std::cout << "exception thrown for parameter error" << std::endl;
}
// solution: use safe range to restrict parameters
std::cout << "Using safe numerics" << std::endl;
try {
// parameters are guarenteed to meet requirements
hours_t hours(10);
minutes_t minutes(83); // interrupt thrown here
// so the following will never throw
safe_convert(hours, minutes);
}
catch(std::exception e){
std::cout
<< "exception thrown when invalid arguments are constructed"
<< std::endl;
}
try {
// parameters are guarenteed to meet requirements when
// constructed on the stack
safe_convert(hours_t(10), minutes_t(83));
}
catch(std::exception e){
std::cout
<< "exception thrown when invalid arguments are constructed on the stack"
<< std::endl;
}
try {
// parameters are guarenteed to meet requirements when
// implicitly constructed to safe types to match function signature
safe_convert(10, 83);
}
catch(std::exception e){
std::cout
<< "exception thrown when invalid arguments are implicitly constructed"
<< std::endl;
}
try {
// the following will never throw as the values meet requirements.
hours_t hours(10);
minutes_t minutes(17);
// the following will never throw because it cannot be called with
// invalid parameters
safe_convert(hours, minutes);
// since safe types can be converted to their underlying unsafe types
// we can still call an unsafe function with safe types
convert(hours, minutes);
// since unsafe types can be implicitly converted to corresponding
// safe types we can just pass the unsafe types. checkin will occur
// when the safe type is constructed.
safe_convert(10, 17);
// note zero runtime overhead once values are constructed
}
catch(std::exception e){
assert(false); // can never arrive here !!!
}
return 0;
}
In the example above the function convert incurrs significant runtime cost every time the function is called. By using "safe" types, this cost is moved to moment when the parameters are constructed. Depending on how the program is constructed, this may totally eliminate extraneous computions for parameter requirement type checking. In this scenario, there is no reason to suppress the checking for release mode and our program can be guarenteed to be always arithmetically correct.