What Is the Meaning of Numeric_Limits<Double>::Digits10

Why is std::numeric_limits::digits10 is one less for int types?

std::numeric_limits<T>::digits10 is the guaranteed number of digits in a sense that a number with that many digits can be represented in type T without causing overflow or loss of information.

E.g. std::numeric_limits<int64_t>::digits10 cannot be 19 becuase 9'223'372'036'854'775'808 has 19 digits but is not representable in int64_t.

In general case such guaranteed value of digits<N> will always suffer from this "one less" discrepancy on platforms where digits<N> is not a power of radix used for internal representation. In non-exotic cases radix is 2. Since 10 is not a power of 2, digits10 is smaller by 1 than the length of the max value.

If std::numeric_limits<T> included digits16 or digits8 these values would've been "precise" for radix 2 platforms.

Why do we need std::numeric_limits::max_digits10?

You seem to be confusing two sources of rounding (and precision loss) with floating point numbers.

Floating point representation

The first one is due to the way floating point numbers are represented in memory, which uses binary numbers for the mantissa and exponent, as you just pointed. The classic example being :

const float a = 0.1f;
const float b = 0.2f;
const float c = a+b;

printf("%.8f + %.8f = %.8f\n",a,b,c);

which will print

0.10000000 + 0.20000000 = 0.30000001

There, the mathematically correct result is 0.3, but 0.3 is not representable with the binary representation. Instead you get the closest number which can be represented.

Saving to text

The other one, which is where max_digits10 comes into play, is for text representation of floating point number, for example, when you do printf or write to a file.

When you do this using the %f format specifier you get the number printed out in decimal.

When you print the number in decimal you may decide how many digits get printed out. In some cases you might not get an exact printout of the actual number.

For example, consider

const float x = 10.0000095f;
const float y = 10.0000105f;
printf("x = %f ; y = %f\n", x,y);

this will print

x = 10.000010 ; y = 10.000010

on the other hand, increasing the precision of printf to 8 digits with %.8f will give you.

 x = 10.00000954 ; y = 10.00001049

So if you wanted to save these two float values as text to a file using fprintf or ofstream with the default number of digits, you may have saved the same value twice where you originally had two different values for x and y.

max_digits10 is the answer to the question "how many decimal digits do I need to write in order to avoid this situation for all possible values ?". In other words, if you write your float with max_digits10 digits (which happens to be 9 for floats) and load it back, you're guaranteed to get the same value you started with.

Note that the decimal value written may be different from the floating point number's actual value (due to the different representation. But it is still guaranteed than when you read the text of the decimal number into a float you will get the same value.

Edit: an example

See the code runt there : https://ideone.com/pRTMZM

Say you have your two floats from earlier,

const float x = 10.0000095f;
const float y = 10.0000105f;

and you want to save them to text (a typical use-case would be saving to a human-readable format like XML or JSON, or even using prints to debug). In my example I'll just write to a string using stringstream.

Let's try first with the default precision :

stringstream def_prec;
def_prec << x <<" "<<y;

// What was written ?
cout <<def_prec.str()<<endl;

The default behaviour in this case was to round each of our numbers to 10 when writing the text. So now if we use that string to read back to two other floats, they will not contain the original values :

float x2, y2;
def_prec>>x2 >>y2;

// Check
printf("%.8f vs %.8f\n", x, x2);
printf("%.8f vs %.8f\n", y, y2);

and this will print

10 10
10.00000954 vs 10.00000000
10.00001049 vs 10.00000000

This round trip from float to text and back has erased a lot of digits, which might be significant. Obviously we need to save our values to text with more precision than this. The documentation guarantees that using max_digits10 will not lose data in the round trip. Let's give it a try using setprecision:

const int digits_max = numeric_limits<float>::max_digits10;
stringstream max_prec;
max_prec << setprecision(digits_max) << x <<" "<<y;
cout <<max_prec.str()<<endl;

This will now print

10.0000095 10.0000105

So our values were saved with more digits this time. Let's try reading back :

float x2, y2;
max_prec>>x2 >>y2;

printf("%.8f vs %.8f\n", x, x2);
printf("%.8f vs %.8f\n", y, y2);

Which prints

10.00000954 vs 10.00000954
10.00001049 vs 10.00001049

Aha ! We got our values back !

Finally, let's see what happens if we use one digit less than max_digits10.

stringstream some_prec;
some_prec << setprecision(digits_max-1) << x <<" "<<y;
cout <<some_prec.str()<<endl;

Here this is what we get saved as text

10.00001 10.00001

And we read back :

10.00000954 vs 10.00000954
10.00001049 vs 10.00000954

So here, the precision was enough to keep the value of x but not the value of y which was rounded down. This means we need to use max_digits10 if we want to make sure different floats can make the round trip to text and stay different.

std::numeric_limits::digits10float and precision after the dot

From cppreference

The value of std::numeric_limits::digits10 is the number of base-10 digits that can be represented by the type T without change, that is, any number with this many decimal digits can be converted to a value of type T and back to decimal form, without change due to rounding or overflow. For base-radix types, it is the value of digits (digits-1 for floating-point types) multiplied by log
10(radix) and rounded down.

And later

The standard 32-bit IEEE 754 floating-point type has a 24 bit fractional part (23 bits written, one implied), which may suggest that it can represent 7 digit decimals (24 * std::log10(2) is 7.22), but relative rounding errors are non-uniform and some floating-point values with 7 decimal digits do not survive conversion to 32-bit float and back: the smallest positive example is 8.589973e9, which becomes 8.589974e9 after the roundtrip. These rounding errors cannot exceed one bit in the representation, and digits10 is calculated as (24-1)*std::log10(2), which is 6.92. Rounding down results in the value 6.

That means, e.g.

cout << std::numeric_limits<float>::digits10; // 6
cout << std::numeric_limits<float>::digits; // 24

the second one is the number of digits in the mantissa while the first one the number of decimal digits that can safely be represented across aforementioned conversions.

TL;DR: it's your first case.

std::numeric_limitsdouble::digits10 = number of digits on the right or left to the radix point?

Actually it's just the number of meaningful digits. Point. You can have 12345678901234.5 or 0.0000123456789012345.

What is std::numeric_limitsT::digits supposed to represent?

After reading the standard a bit more and thinking about this, I believe I have the best answer, but I am not certain.

First, the definition of digits, taken from the latest C++14 draft standard, N3797, § 18.3.2.4:

static constexpr int digits;

8 Number of radix digits that can be represented without change.

9 For integer types, the number of non-sign bits in the representation.

10 For floating point types, the number of radix digits in the mantissa

The case of bounded::integer<-100, 5> is the same as for bounded::integer<0, 5>, which would give a value of 2.

For the case of bounded::integer<16, 19>, digits should be defined as 0. Such a class cannot even represent a 1-bit number (since 0 and 1 aren't in the range), and according to 18.3.2.7.1:

All members shall be provided for all specializations. However, many values are only required to be meaningful under certain conditions (for example, epsilon() is only meaningful if is_integer is false). Any value that is not "meaningful" shall be set to 0 or false.

I believe that any integer-like class which does not have 0 as a possible value cannot meaningfully compute digits and digits10.

Another possible answer is to use an information theoretic definition of digits. However, this is not consistent with the values for the built-in integers. The description explicitly leaves out sign bits, but those would still be considered a single bit of information, so I feel that rules out this interpretation. It seems that this exclusion of the sign bit also means that I have to take the smaller in magnitude of the negative end and the positive end of the range for the first number, which is why I believe that the first question is equivalent to bounded::integer<0, 5>. This is because you are only guaranteed 2 bits can be stored without loss of data. You can potentially store up to 6 bits as long as your number is negative, but in general, you only get 2.

bounded::integer<16, 19> is much trickier, but I believe the interpretation of "not meaningful" makes more sense than shifting the value over and giving the same answer as if it were bounded::integer<0, 3>, which would be 2.

I believe this interpretation follows from the standard, is consistent with other integer types, and is the least likely to confuse the users of such a class.

To answer the question of the use cases of digits, a commenter mentioned radix sort. A base-2 radix sort might expect to use the value in digits to sort a number. This would be fine if you set digits to 0, as that indicates an error condition for attempting to use such a radix sort, but can we do better while still being in line with built-in types?

For unsigned integers, radix sort that depends on the value of digits works just fine. uint8_t has digits == 8. However, for signed integers, this wouldn't work: std::numeric_limits<int8_t>::digits == 7. You would also need to sort on that sign bit, but digits doesn't give you enough information to do so.



Related Topics



Leave a reply



Submit