![]() |
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 and object values upon entry to and exit from every function. This can slow the program down considerably which in turn 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 which defeats the guarantee of correctness which we are seeking here! Programming by Contract will never be accepted by programmers as long as it is associated with significant additional runtime cost.
The Safe Numerics Library has facilities which, in many cases, can check guarantee parameter requirements with little or no runtime overhead. Consider the following example:
#include <cassert>
#include <stdexcept>
#include <sstream>
#include <iostream>
#include "../include/safe_range.hpp"
// NOT using safe numerics - enforce program contract explicitly
// 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;
}
// Use safe numeric to enforce program contract automatically
// 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.
const hours_t hours(10);
const minutes_t minutes(17);
// note zero runtime overhead once values are constructed
// the following will never throw because it cannot be called with
// invalid parameters
safe_convert(hours, minutes); // zero runtime overhead
// since safe types can be converted to their underlying unsafe types
// we can still call an unsafe function with safe types
convert(hours, minutes); // zero (depending on compiler) runtime overhead
// 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); // runtime cost in creating parameters
}
catch(std::exception e){
std::cout << "error detected!" << std::endl;
}
return 0;
}
In the example above the function convert incurs 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 computations for parameter requirement type checking. In this scenario, there is no reason to suppress the checking for release mode and our program can be guaranteed to be always arithmetically correct.