numeric_cast


Header: "boost/cast.hpp"

Conversions between integral types can often produce unexpected results. For example, a long can typically hold a much greater range of values than a short, so what happens when assigning a long to a short and the long's value is outside of short's range? The answer is that the result is implementation defined (a nice term for "you can never know for sure"). Signed to unsigned conversions between same size integers are fine, so long as the signed value is positive, but what happens if the signed value is negative? It turns into a large unsigned value, which is indeed a problem if that was not the intention. numeric_cast helps ensure valid conversions by testing whether the range is preserved and by throwing an exception if it isn't.

7. The C++ Standard covers promotions and conversions for numeric types in §4.5-4.9.

Before we can fully appreciate numeric_cast, we must understand the rules that govern conversions and promotions of integral types. The rules are many and sometimes subtlethey can trap even the experienced programmer. Rather than stating all of the rules7 and then carry on, I'll give you examples of conversions that are subject to undefined or surprising behavior, and explain which rules the conversions adhere to.

When assigning to a variable from one of a different numeric type, a conversion occurs. This is perfectly safe when the destination type can hold any value that the source can, but is unsafe otherwise. For example, a char generally cannot hold the maximum value of an int, so when an assignment from int to char occurs, there is a good chance that the int value cannot be represented in the char. When the types differ in the range of values they can represent, we must make sure that the actual value to convert is in the valid range of the destination type. Otherwise, we enter the land of implementation-defined behavior; that's what happens when a value outside of the range of possible values is assigned to a numeric type.[8] Implementation-defined behavior means that the implementation is free to do whatever it wants to; different systems may well have totally different behavior. numeric_cast can ensure that the conversions are valid and legal or they will not be allowed.

[8] Unsigned arithmetic notwithstanding; it is well defined for these cases.

Usage

numeric_cast is a function template that looks like a C++ cast operator and is parameterized on both the destination and source types. The source type can be implicitly deduced from the function argument. To use numeric_cast, include the header "boost/cast.hpp". The following two conversions use numeric_cast to safely convert an int to a char, and a double to a float.

 char c=boost::numeric_cast<char>(12); float f=boost::numeric_cast<float>(3.001); 

One of the most common numeric conversion problems is assigning a value from a type with a wider range than the one being assigned to. Let's see how numeric_cast can help.

Assignment from a Larger to a Smaller Type

When assigning a value from a larger type (for example, long) to a smaller type (for example, short), there is a chance that the value is too large or too small to be represented in the destination type. If this happens, the result is (yes, you've guessed it) implementation-defined. We'll talk about the potential problems with unsigned types later; let's just start with the signed types. There are four built-in signed integral types in C++:

  • signed char

  • short int (short)

  • int

  • long int (long)

There's not much one can say with absolute certainty about which type is larger[9] than others, but typically, the listing is in increasing size, with the exception that int and long often hold the same range of values. They're all distinct types, though, even if they're the same size. To see the sizes on your system, use either sizeof(T) or std::numeric_limits<T>::max() and std::numeric_limits<T>::min().

[9] Of course, the ranges of signed and unsigned types are different even if the types have the same size.

When assigning one signed integral type to another, the C++ Standard says:

"If the destination type is signed, the value is unchanged if it can be represented in the destination type (and bitfield width); otherwise, the value is implementation-defined."[10]

[10] See §4.7.3 of the C++ Standard.

The following piece of code gives an example of how these implementation-defined values are often the result of seemingly innocent assignments, and finally how they are avoided with the help of numeric_cast.

 #include <iostream> #include "boost/cast.hpp" #include "boost/limits.hpp" int main() {   std::cout << "larger_to_smaller example\n";   // Conversions without numeric_cast   long l=std::numeric_limits<short>::max();   short s=l;   std::cout << "s is: " << s << '\n';   s=++l;   std::cout << "s is: " << s << "\n\n";   // Conversions with numeric_cast   try {     l=std::numeric_limits<short>::max();     s=boost::numeric_cast<short>(l);     std::cout << "s is: " << s << '\n';     s=boost::numeric_cast<short>(++l);     std::cout << "s is: " << s << '\n';   }   catch(boost::bad_numeric_cast& e) {     std::cout << e.what() << '\n';   } } 

Utilizing std::numeric_limits, the long l is initialized to the maximum value that a short can possibly hold. That value is assigned to the short s and printed. After that, l is incremented by one, which means that it now holds a value that cannot be represented by a short; it is outside the range of values that a short can represent. After assigning from the new value of l to s, s is printed again. What's the value, you might ask? Well, because the assignment results in implementation-defined behavior, that depends upon the platform. On my system, with my compiler, it turns out that the result is a large negative value, which implies that the value has been wrapped. There's no telling[11] what it will be on your system without running the preceding code. Next, the same operations are performed again, but this time using numeric_cast. The first cast succeeds, because the value is within range. The second, however, fails, and the result is that an exception of type bad_numeric_cast is thrown. The output of the program is as follows.

[11] Although the behavior and value demonstrated here is very common on 32-bit platforms.

 larger_to_smaller example s is: 32767 s is: -32768 s is: 32767 bad numeric cast: loss of range in numeric_cast 

A benefit that might be even more important than dodging the implementation-defined value is that numeric_cast helps us avoid errors that are otherwise very hard to trap. The strange value could be passed on to other parts of the application, perhaps working in some cases, but almost certainly yielding the wrong result. Of course, this only happens for certain values, and if those values seldom occur, the error will be very hard to track down. Such errors are insidious because they happen only for some values rather than all of the time.

Loss of precision or range is not unusual, and if you aren't absolutely certain that a value too large or too small for the destination type will never be assigned, numeric_cast is the tool for you. You can even use numeric_cast when it's unnecessary; the maintenance programmer may not have the same insight as you do. Note that although we have covered only signed types here, the same principles apply to unsigned integral types, too.

Special CaseUnsigned Integral Type As Destination

Unsigned integral types have a very interesting propertyany numeric value can be legally assigned to them! There is no notion of positive or negative overflow when it comes to unsigned types. They are reduced modulo the number that is one greater than the largest value of the destination type. Say what? An example in code might make it clearer.

 #include <iostream> #include "boost/limits.hpp" int main() {   unsigned char c;   long l=std::numeric_limits<unsigned char>::max()+14;   c=l;   std::cout << "c is:       " << (int)c << '\n';   long reduced=l%(std::numeric_limits<unsigned char>::max()+1);   std::cout << "reduced is: " << reduced << '\n'; } 

The output of running the program follows:

 c is:       13 reduced is: 13 

The example assigns a value that is certainly greater than what an unsigned char can hold, and then that same value is calculated. The workings of the assignment is shown in this line:

 long reduced=l%(std::numeric_limits<unsigned char>::max()+1); 

This behavior is often referred to as value wrapping. If you want to use this property of unsigned integral types, there is no need to use numeric_cast in those situations. Furthermore, numeric_cast won't accept it. numeric_cast's intent is to catch errors, and this is considered an error because it is the result of a typical user misunderstanding. If the destination type cannot represent the value that is being assigned, a bad_numeric_cast exception is thrown. Just because unsigned integer arithmetic is well defined doesn't make the programmer's error less fatal.[12] For numeric_cast, the important aspect is to preserve the actual value.

[12] The point: If you really want value wrapping, don't use numeric_cast.

Mixing Signed and Unsigned Integral Types

It's easy to have fun[13] when mixing signed and unsigned types, especially when performing arithmetic operations. Plain assignments offer some clever pitfalls, too. The most common problem is assigning a negative value to an unsigned type. The result is almost certainly not what was intended. Another issue is when assigning from an unsigned type to a signed type of the same size. Somehow, it seems to be easy to forget that the unsigned type can hold higher values than the signed counterpart. It's even easier to forget the types involved in an expression or function call. Here's an example that shows how these common errors are caught by numeric_cast.

[13] This is a highly subjective matter, of course, and your mileage may vary.

 #include <iostream> #include "boost/limits.hpp" #include "boost/cast.hpp" int main() {   unsigned int ui=std::numeric_limits<unsigned int>::max();   int i;   try {     std::cout << "Assignment from unsigned int to signed int\n";     i=boost::numeric_cast<int>(ui);   }   catch(boost::bad_numeric_cast& e) {     std::cout << e.what() << "\n\n";   }   try {     std::cout << "Assignment from signed int to unsigned int\n";     i=-12;     ui=boost::numeric_cast<unsigned int>(i);   }   catch(boost::bad_numeric_cast& e) {     std::cout << e.what() << "\n\n";   } } 

The output clearly shows that the errors were trapped as expected.

 Assignment from unsigned int to signed int bad numeric cast: loss of range in numeric_cast Assignment from signed int to unsigned int bad numeric cast: loss of range in numeric_cast 

The basic rule to follow is simple: Whenever a type conversion is performed between different types, make the conversion safe by using numeric_cast.

Floating Point Types

numeric_cast does not help with loss of precision when converting between floating point types. The reason is that the conversions between float, double, and long double aren't susceptible to the implicit conversions of integer types. It is important to remember that because it is easy to think that the following would result in an exception being thrown.

 double d=0.123456789123456; float f=0.123456; try {   f=boost::numeric_cast<float>(d); }   catch(boost::bad_numeric_cast& e) {     std::cout << e.what(); } 

No exception will be thrown when running this code. The conversion from double to float results in a loss of precision on most implementations, although it's not guaranteed by the C++ Standard. All we know for sure is that a double has at least the precision of a float.

What about conversions from floating point types to integer types? When a floating point type is converted to an integer type, it is truncated; the fractional part is discarded. numeric_cast performs the same checking on the truncated value and destination type range as it would for two integral types.

 double d=127.123456789123456; char c; std::cout << "char type maximum: "; std::cout << (int)std::numeric_limits<char>::max() << "\n\n"; c=d; std::cout << "Assignment from double to char: \n"; std::cout << "double: " << d << "\n"; std::cout << "char:   " << (int)c << "\n"; std::cout << "Trying the same thing with numeric_cast:\n"; try {   c=boost::numeric_cast<char>(d);   std::cout << "double: " << d;   std::cout << "char:   " << (int)c; }   catch(boost::bad_numeric_cast& e) {     std::cout << e.what(); } 

Doing range checks to ensure valid assignments like the preceding ones is a daunting task. Although the rules seem simple, there are many combinations that must be considered. For example, a test for floating point to integral assignment could look like this:

 template <typename INT, typename FLOAT>   bool is_valid_assignment(FLOAT f) {     return std::numeric_limits<INT>::max() >=       static_cast<INT>(f);   } 

Even though I just mentioned that the fractional part is discarded when a floating point type is converted, it's easy to miss the error in this implementation. This is the nature of conversions and promotions of arithmetic types. Omitting the static_cast makes the test work correctly, because the result of numeric_limits<INT>::max then is converted to the floating point type.[14] If the floating point value is converted to an integral type, it is truncated; in other words, the bug in this function is that any fractional part is lost.

[14] As a result of the usual arithmetic conversions.

Summary

numeric_cast offers efficient, range-checked conversions between arithmetic types. For those cases where the destination type can hold all values that the source type can, there is no efficiency penalty for using numeric_cast. It only has impact when the destination type can hold only a subset of the values of the source type. When a conversion fails, numeric_cast signals the failure by throwing an exception of type bad_numeric_cast. As there are so many intricate rules governing conversions between numeric types, ensuring correctness is vital.

When to use numeric_cast:

  • When assigning/comparing unsigned and signed types

  • When assigning/comparing integral types of different sizes

  • When assigning a function return type to a numeric variable, to protect against future changes to the function

Notice a pattern here? Mimicking existing language and library names and behavior is a powerful technique for simplifying learning and usage, but it also requires a lot of thought. Augmenting the built-in C++ casts is a walk along a narrow road; straying comes at a high price. Making something follow the syntactic and semantic rules of the language implies responsibility. In fact, for novices, there might not be any difference at all between built-in casts and functions that look like casts, so if the behavior is incorrect it can wreak havoc. numeric_cast has the similar syntax and semantics of static_cast, dynamic_cast, and reinterpret_cast. If it looks and behaves like a cast, it is a cast, and this particular one is a nice addition to that family.



    Beyond the C++ Standard Library(c) An Introduction to Boost
    Beyond the C++ Standard Library: An Introduction to Boost
    ISBN: 0321133544
    EAN: 2147483647
    Year: 2006
    Pages: 125

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net