0

I have this function:

template<typename T> // T can be float, double or long double
void printAllDigits(T value)
{
    std::cout << std::fixed << std::setprecision(999) << value;
}

It's a dumb implementation to print all digits of a floating point value.

This has some problems:

  • I can't guarantee that it works for all float types. It seems to work for float (999 digits is probably enough), and maybe works for double, but certainly does not work for long double (std::numeric_limits<long double>::min() is printed as "0.", followed by 999 zeros).
  • It is extremely wasteful, because e.g. for float it always prints a ton of trailing zeros, even though those can never be non-zero.

But it does some things right:

  • I never want scientific notation, so std::fixed makes sense.
  • I have code that strips the trailing zeros (akin to what is sugessted in Remove trailing zero in C++), which works well with this approach.
  • I don't have to write down separate code paths or constants for the different float types.
  • If the argument to setprecision is large enough, this actually prints all digits without rounding.
  • I can copy the output, plonk it back into a code file (and make sure to add ".0f" and such where necessary) and get the same floating point value.
  • "Unnecessary" digits are not rounded away. printAllDigits(0.1f) prints "0.100000001490116119384765625000000...". Printing "0.1" would be sufficient to get back to the original float value, but I still need the function to print all of those digits.

How can I make that function less wasteful while maintaining my requirements?

std::numeric_limits<T>::max_digits10 is incorrect, since it is too small! std::numeric_limits<float>::min() gets printed as "0.000000000" instead of "0.0000000000000000000000000000000000000117549435082228750796873653722224567781866555677208752150875170627841725945472717285156250000000..."

ildjarn
  • 62,044
  • 9
  • 127
  • 211
Niko O
  • 406
  • 3
  • 15
  • I do not believe the C++ standard guarantees a way to do what you request other than by computing it yourself. This is because the standard refers to the C standard for formatting features (I would have to dig into the details of the stream formatting specifications to confirm this), and the C standard only requires an implementation to be able to correctly produce `DECIMAL_DIG` digits. `DECIMAL_DIG` is the number of digits required to uniquely identify each value representable in the widest supported floating-point type. Thus, it guarantees unique identification but not correct digits. – Eric Postpischil Feb 21 '22 at 13:42
  • Further, the requirement that “I can copy the output, plonk it back into a code file (and make sure to add ".0f" and such where necessary) and get the same floating point value.” is problematic. If you do have all the digits, that should work. But if you just have enough digits to uniquely identify the number, the C++ standard does not require that the round trip work. C++ 2017 draft N4659 5.13.4 1 says “… the result is the scaled value if representable, else the larger or smaller representable value nearest the scaled value, chosen in an implementation-defined manner. ” – Eric Postpischil Feb 21 '22 at 13:45
  • (The “scaled value” is the nominal value of the constant with the exponent applied if it is present. E.g., the “scale value” of `3.4e2` is 3400, and the scaled value of `.123` is .123.) – Eric Postpischil Feb 21 '22 at 13:46
  • 1
    What are you really trying to do? If you were trying to produce output useful for a human, producing many digits would not be good. If you were trying to produce output useful for a computer to parse, there is no reason to avoid scientific notation. If you to ensure round-tripping of floating-point conversions, then the `hexfloat` format is useful. – Eric Postpischil Feb 21 '22 at 13:53
  • @EricPostpischil Thanks for the heads-up! I am trying to find a well-defined, unsurprising, predictable, sane, you-won't-pull-your-hair-out-in-frustration subset of floating point operations. To do that, I need to know what exactly the numbers are. It's extremely frustrating to look at 0.125 and not know whether that's **really** 0.125 or actually 0.1250001 rounded to < 7 digits. I need predictable (and ideally also useful) behavior, which, unfortunately, is very difficult to get in C++. – Niko O Feb 21 '22 at 14:09
  • The IEEE-754 standard defines floating-point operations in a well-defined, largely unsurprising, predictable, and sane way. It leaves a few things incompletely specified, like whether tininess for underflow is detected before or after rounding. Most of the frustration people experience with elementary floating-point operations is due to lack of knowledge, formatting choices by language standards that conceal or camouflage information, and lack of programming language conformance to IEEE-754 (in the standard and/or the implementations). – Eric Postpischil Feb 21 '22 at 16:20
  • Regarding those formatting choices by languages, once one does have knowledge of floating-point arithmetic, the result of formatting are no longer surprising or unpredictable. Formatting is largely an issue because of how it misleads people not familiar with floating-point arithmetic; it is not much of a problem for practitioners. – Eric Postpischil Feb 21 '22 at 16:25
  • I suggest studying the [IEEE-754 standard](https://en.wikipedia.org/wiki/IEEE_754) or [Handbook of Floating-Point Arithmetic](https://smile.amazon.com/Handbook-Floating-Point-Arithmetic-Jean-Michel-Muller-dp-3030095134/dp/3030095134/ref=mt_other?_encoding=UTF8&me=&qid=1645460628). – Eric Postpischil Feb 21 '22 at 16:25

3 Answers3

2

Assuming that the implementation uses radix 2 for the floating point,
if (std::numeric_limits<T>::radix == 2)
then writing ALL the decimal digits for ALL possible values would require:

std::cout << std::setprecision(std::numeric_limits<T>::digits - std::numeric_limits<T>::min_exponent);

I suspect that the formulation would be the same for radix == 10, but I cannot check this (no implementation at hand).

I wonder why you want to print all the decimals. If it's just to reconstruct the value unchanged, this is not necessary. Using a mixture of scientific notation with a precision std::numeric_limits<T>::max_digits10 should do the job. Otherwise, there are well known algorithm to print just enough decimal digits. They are used in the main REPL languages (see python repr, or how java, javascript, some Smalltalk, etc... print the floating point values), unfortunately, they are not part of a standard C++ library AFAIK.

aka.nice
  • 9,100
  • 1
  • 28
  • 40
  • 1
    Per my comment on the question, this may produce enough digits to show the exact value of the number, but the digits produced might not represent the exact value of the number, because the C++ standard does not require a correct calculation of the digits beyond the number needed to uniquely identify values in the widest supported floating-point type. – Eric Postpischil Feb 21 '22 at 13:50
  • Can you give an example for a number such that printing with `std::fixed << std::setprecision(sufficiently large) << number` and truncating trailing zeros will result in a string larger than it would with `std::numeric_limits::min()`? Because for `float`, `double` and `long double`, the length of `min()` is 128, 1024 and 16384 respectively, which is 21, 50 and 110 shorter than your proposed 149, 1074 and 16494. As to why: I want to know what exactly my numbers mean to the computer. I can (and will) later decide what information to round away, but first I need to know what that is. – Niko O Feb 21 '22 at 13:52
  • We could use `ilogb` instead of `min_exponent` to tailor the number of digits to the number. (If the number is zero or has magnitude greater than 1, request zero digits after the decimal point. Eitherwise, use the sum of `…::digits` and the negative of the exponent returned by `ilogb`.) That will not be exact, but it will be greater than or equal to the number of digits needed and not exceed it by more than `…::digits`, avoiding a lot of wasted zeros. – Eric Postpischil Feb 21 '22 at 15:22
  • @EricPostpischil yes, i thought of using `ilogb`, in which case we would have at most `std::numeric_limits::digits - 1` trailing zeroes depending on the trailing zeroes in significand bits. Or even, obtain an integer significand via `scalbn`, count the trailing zero bits and adjust the precision accordingly. But that means having an integer large enough for storing the significand, or splitting the scalbn into several operations, etc... Too much work for a simple SO answer, hard to generalize to other radix, etc... – aka.nice Feb 21 '22 at 16:10
  • @NikoO in IEEE754, the smallest representable unnormalized double above zero `std::numeric_limits::denorm_min()` is 2^-1074. If you want to print it in decimal form, that is 5^1074/10^1074, and you obviously need 1074 decimals after the fraction separator to print that exactly. `std::numeric_limits::min()` has 52 trailing significand bits set to zero, so only require 1022 digits after fraction separator, +2 for leading `0.`, but `std::nextafter(std::numeric_limits::min(),1.0)` requires 1074 fracton digits +2 for leading `0.` – aka.nice Feb 21 '22 at 16:37
0

Here is another answer that will avoid removing the trailing zeroes.

The idea is to scale the value (multiply by radix^scale) until we obtain a significand int.frac with a null fraction part.

This works for radix == 2 or 10.

// return the number of fractional decimal digits required to print a floating point exact value
template<class FloatType> int required_precision( FloatType value )
{
    assert( std::numeric_limits<FloatType>::radix == 2 ||
            std::numeric_limits<FloatType>::radix == 10 );
    if (! std::isfinite(value) ) return 0;
    // exponent of 0.0 is implementation defined, so don't rely on ilogb
    if (value == FloatType(0) ) return 1;
    // use ilogb for initial guess of scale                                                                                                                                         int exponent = std::ilogb( value );
    int scale =  - exponent;
    FloatType significand = std::scalbn( std::abs(value) , scale );
    // scale up until fraction part is null
    while( significand-std::trunc(significand) != FloatType(0) ) {
        scale += 1;
        significand = std::scalbn( significand - std::trunc(significand) , 1 );
    }
    // print at least 1 fractional zero for case of integral value
    return std::max(1,scale);
}

This should work whatever the radix.

If the radix is 2, and if the number of digits does not exceed that of unsigned long long, then we can scale only once so as to obtain an integer significand, and count trailing zeroes of that significand to adjust the scale.

template<class FloatType> int required_precision( FloatType value )
{
    assert( std::numeric_limits<FloatType>::radix == 2 &&
            std::numeric_limits<FloatType>::digits <= std::numeric_limits<unsigned long long>::digits );
    if (! std::isfinite(value) ) return 0;
    // exponent of 0.0 is implementation defined, so don't rely on ilogb
    if (value == FloatType(0) ) return 1;
    int exponent = std::ilogb( value );
    // quick check if exponent is greater than max number of digits, then it is an integral value
    if( exponent >= std::numeric_limits<FloatType>::digits ) return 1;
    // scale significand to integer
    int scale =  std::numeric_limits<FloatType>::digits - 1 - exponent;
    FloatType significand = std::scalbn( std::abs(value) , scale );
    unsigned long long int_significand = static_cast<unsigned long long>(significand);
    // replace this by your own trick to count trailing zeroes if not compiling with gcc/g++
    return std::max(1,scale - __builtin_ctzll( int_significand ));
}

if unsigned long long has not enough bits to hold the significand of some FloatType, then the scaling has to be split with a solution in between the 2 above:

template<class FloatType> int required_precision( FloatType value )
{
    assert( std::numeric_limits<FloatType>::radix == 2 );
    if (! std::isfinite(value) ) return 0;
    if (value == FloatType(0) ) return 1;
    int exponent = std::ilogb( value );
    if( exponent >= std::numeric_limits<FloatType>::digits ) return 1;
    // scale significand to integer : care to not overflow UNSIGNED_LONG_LONG_MAX
    int max_bits = std::numeric_limits<unsigned long long>::digits;
    int scale =  max_bits - 1 - exponent;
    FloatType significand = std::scalbn( std::abs(value) , scale );
    while( significand-std::trunc(significand) != FloatType(0) ) {
        scale += max_bits;
        significand = std::scalbn( significand - std::trunc(significand) , max_bits );
    }
    unsigned long long int_significand = static_cast<unsigned long long>(significand);
    return std::max(1,scale - __builtin_ctzll( int_significand ));
}
aka.nice
  • 9,100
  • 1
  • 28
  • 40
0

999 digits is probably enough

Typical float needs about 150 digits to the right of the decimal point and double needs about 1075 to print the exact value in some cases. Setting precision that high may not produce the exact output and one will need specialized code to do so.

Forego the "I never want scientific notation" and use decimal floating point notation, printing with at least 6 (float), 9 (double) significant places is enough to round-trip values from floating-point to text to floating point.

Or use hexadecimal notation.

chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256