Eliminating Runtime PenaltyUp until now, we've mostly focused on detecting when incorrect results
are produced and handling these occurrences either by throwing an exception
or invoking some designated function. We've achieved our goal of detecting
and handling arithmetically incorrect behavior - but at cost of checking
many arithmetic operations at runtime. It is a fact that many C++
programmers will find this trade-off unacceptable. So the question arises as
to how we might minimize or eliminate this runtime penalty.The first step is to determine what parts of a program might invoke
exceptions. The following program is similar to previous examples but uses a
special exception policy: loose_trap_policy.Now,
any expression which might fail at runtime is flagged with a
compile time error. There is no longer any need for try/catch
blocks. Since this program does not compile, the library absolutely guarantees that no
arithmetic expression will yield incorrect results.
Furthermore, it is absolutely guaranteed that no
exception will ever be thrown. This is our original goal.Now all we need to do is make the program compile. There are a couple
of ways to achieve this.Using safe_range
and safe_literalWhen 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.safe_signed_range 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.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 loose_trap_policy
will mean that any assignment to z which could be outside its legal
range will result in a compile time error.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] = 77Take 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.Using Automatic Type PromotionThe 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 size of the signed operand is larger than the size of the
signed operand, the type of the result will be signed. Otherwise, the
type of the result will be unsigned.Convert the type each operand to the type of the result,
expanding the size as necessary.Perform the operation the two resultant operands.So the type of the result of some binary operation may be different
than the types of either or both of the original operands.If the values are large, the result can exceed the size that the
resulting integer type can hold. This is what we call "overflow". The
C/C++ standard characterizes this as undefined behavior and leaves to
compiler implementors the decision as to how such a situation will be
handled. Usually, this means just truncating the result to fit into the
result type - which sometimes will make the result arithmetically
incorrect. However, depending on the compiler and compile time switch
settings, such cases may result in some sort of run time exception or
silently producing some arbitrary result.The complete signature for a safe integer type is:template <
class T, // underlying integer type
class P = native, // type promotion policy class
class E = default_exception_policy // error handling policy class
>
safe;
The promotion rules for arithmetic operations are implemented in the
default native
type promotion policy are consistent with those of standard C++Up until now, we've focused on detecting when an arithmetic error
occurs and invoking an exception 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 type, 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 at compile time.From this interval we can select a new type which can be
guaranteed to hold the result and use this for the calculation. This
is more or less equivalent to the following code:int x, y;
int z = x + y // could overflow
// so replace with the following:
int x, y;
long z = (long)x + (long)y; // can never overflowOne could do this by editing his code manually as above, but
such a task would be tedious, error prone, non-portable and leave
the resulting code hard to read and verify. Using the automatic
type promotion policy will achieve the equivalent result without
these problems.When using the automatic
type promotion policy, with a given a binary operation, we silently
promote the types of the operands to a wider result type so the result
cannot overflow. This is a fundamental departure from the C++ Standard
behavior.If the interval of the result cannot be guaranteed to fit 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. So while our automatic
type promotion policy might eliminate exceptions in our example above, it
wouldn't be guaranteed to eliminate them for all programs.Using the loose_trap_policy
exception policy will produce a compile time error anytime it's possible
for an error to occur.This small example illustrates how to use automatic type promotion
to eliminate all runtime penalty.the automatic
type promotion policy has rendered the result of the sum of two
integers as a safe<long> type.our program compiles without error - even when using the loose_trap_policy
exception policy. This is because since a long can always
hold the result of the sum of two integers.We do not need to use the try/catch idiom to handle
arithmetic errors - we will have no exceptions.We only needed to change two lines of code to achieve our goal
of guaranteed program correctness with no runtime penalty.The above program produces the following output:example 82:
x = <int>[-2147483648,2147483647] = 2147483647
y = <int>[-2147483648,2147483647] = 2
x + y = <long>[-4294967296,4294967294] = 2147483649
Note that if any time in the future we were to change
safe<int> to safe<long long> the program could now overflow.
But since we're using loose_trap_policy
the modified program would fail to compile. At this point we'd have to
alter our yet program again to eliminate run time penalty or set aside our
goal of zero run time overhead and change the exception policy to default_exception_policy
.Note that once we use automatic type promotion, our programming
language isn't C/C++ anymore. So don't be tempted to so something like the
following:// DON'T DO THIS !
#if defined(NDEBUG)
using safe_t = boost::numeric::safe<
int,
boost::numeric::automatic, // note use of "automatic" policy!!!
boost::numeric::loose_trap_policy
>;
#else
using safe_t = boost::numeric::safe<int>;
#endif
Mixing ApproachesFor purposes of exposition, we've divided the discussion of how to
eliminate runtime penalties by the different approaches available. A
realistic program could likely include all techniques mentioned above.
Consider the following:As before, we define a type safe_t to reflect our
view of legal values for this program. This uses the automatic
type promotion policy as well as the loose_trap_policy
exception policy to enforce elimination of runtime penalties.The function f accepts only arguments of type
safe_t so there is no need to check the input values.
This performs the functionality of programming by contract with no
runtime cost.In addition, we define input_safe_t to be used
when reading variables from the program console. Clearly, these can
only be checked at runtime so they use the throw_exception policy.
When variables are read from the console they are checked for legal
values. We need no ad hoc code to do this, as these types are
guaranteed to contain legal values and will throw an exception when
this guarantee is violated. In other words, we automatically get
checking of input variables with no additional programming.On calling of the function f, arguments of type
input_safe_t are converted to values of type
safe_t . In this particular example, it can be
determined at compile time that construction of an instance of a
safe_t from an input_safe_t can never
fail. Hence, no try/catch block is necessary. The usage
of the loose_trap_policy
policy for safe_t types guarantees this to be true at
compile time.Here is the output from the program when values 12 and 32
are input from the console:example 84:
type in values in format x y:33 45
x<signed char>[-24,82] = 33
y<signed char>[-24,82] = 45
z = <short>[-48,164] = 78
(x + y) = <short>[-48,164] = 78
(x - y) = <signed char>[-106,106] = -12
<short>[-48,164] = 78