Detecting the Properties and Limits of Numeric Datatypes

++ has about 10 distinct integral datatypes and three more floating point datatypes. Each datatype has different numeric properties such as the numeric range, the maximum number of digits it can represent, and a different precision. These properties are critical in financial, scientific, graphical, and DSP applications. This article will discuss how to obtain the numeric properties of fundamental datatypes programmatically by using the library.


“How many digits can a double store?”, “What is the maximum positive value that signed long can represent?”, and similar questions are critical to the design of your application. How do you get the answers to these questions in a systematic and portable fashion?


Use the standard library to obtain the numeric properties of built-in datatypes.

Floating Point Blues
C++ floating point datatypes have finite precision. Hardware-specific vagaries contribute their own share to the truncation and rounding of floating point datatypes. Now you understand why the result of the expression 2.0/3.0 is approximated as 0.66666666666666663 on my machine. This “digital noise” is a fertile source of many bugs. Consider:

double d1=2., d2=3.;d1/=d2;  // 2/3if (d1*10==(20./d2)) //condition should be true, alas{ //unreachable code do_equal();}

The code between the pair of braces is never executed because the subexpressions on both sides of the == operator yield slightly different results: d1*10 is 6.6666666666666661 whereas 20./d2 is 6.6666666666666670. The difference between these two numbers is an artifact of the truncation and approximation incurred by floating point arithmetic. As a workaround, you can replace floating point datatypes with scaled integers. Sometimes however, this isn’t an option. Consider a spreadsheet that evaluates complex formulas?it must use floating point types. For such cases, the epsilon constant comes in handy. Epsilon is the difference between 1 and the smallest value greater than 1 that is representable for a given datatype. For example, the epsilon value for type double is:

#include #include using namespace std;cout << numeric_limits::epsilon( ) << endl; //output: 2.22045e-016

To neutralize the effect of digital noise in the if-statement, replace the == operator with an expression that checks whether the two values are roughly identical:

if ( ((d1*10)-(20.0/d2)) <= numeric_limits::epsilon()){  do_equal();}

If the result of the expression (d1*10)-(20.0/d2) isn't higher than the epsilon for type double, it's practically zero. Hence, the two subexpressions have identical values. Using this technique you can fine-tune the error threshold. For example, if a difference of one billionth or less is insignificant in your application, try the following:

const double BILLIONTH=1./1000000000;if ( ((d1*10)-(20.0/d2)) <= BILLIONTH)

Remember though that epsilon is always the lowest deviation threshold.

Better the double You Know
One of the criteria for choosing a floating point datatype is the number of decimal digits it can store without precision loss. For example, suppose your application must support decimal numbers with up to 16 digits. Should you use double, long double or perhaps there's no escape from using a user-defined type? To answer this question, use the numeric_limits::digits10 constant which reports the number of decimal digits that a type can represent without loss of precision:

cout<::digits10<

It appears that double doesn't offer this precision. Will long double save the day?

cout<::digits10<

Yes, it will.

Notice that digits10 works with integral types as well:

cout<::digits10<

Max and Min
The maximum and minimum values that a type can represent are obtained by calling the numeric_limits::max() and numeric_limits::min() member functions:

cout<::max()<

Unlimited
In IEC 559-compliant implementations, floating point datatypes have an agreed-upon value which represents "not a number", or NaN. NaN is a special encoding that represents an illegal number. This can be the result of an illegal instruction, or it could be deliberately chosen to indicate a value that should be ignored, discarded etc. A NaN is said to be quiet if its presence in an expression doesn't raise a signal. Otherwise, it's a signaling NaN. The following example checks which type of NaN is supported on the target platform and then assigns that NaN value to a variable:

double d=0;if(numeric_limits::has_quiet_NaN) d=numeric_limits::quiet_NaN();else if (numeric_limits::has_signaling_NaN) d=numeric_limits::signaling_NaN();else cerr<<"NaN for double isn't supported";

Infinity is another special case. Infinity is the result of division by zero or other operations. The following code checks whether the target machine defines a special infinity code and then assigns this value to a variable:

float f=0;if(numeric_limits::has_infinity) f=numeric_limits::infinity();else cerr<<"infinity for float isn't supported";
Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Overview

Recent Articles:

©2023 Copyright DevX - All Rights Reserved. Registration or use of this site constitutes acceptance of our Terms of Service and Privacy Policy.

Sitemap