Printing double aka the most difficult problem in computer science

A few years ago I discovered this StackOverflow question: How do I print a double value with full precision using cout? I was shocked to see how wrong most of the answers were so I answered it myself back then. Recently I stumbled upon it again and decided to write this blog post explaining the problems with some of the top answers.

Consider the (now removed) top answer with almost 500 upvotes. The first part of it gives us this method:

double d = 3.14159265358979;
cout.precision(17);
cout << "Pi: " << fixed << d << endl;

It is funny to see a weirdly rounded approximation of π in the question about precision but putting it aside, is this answer correct? Let’s check!

Plugging it in godbolt and fixing obvious mistakes like missing namespace qualification gives us:

Pi: 3.14159265358979001

It seems to work, doesn’t it? Let’s check another number:

double d = 1e-20;
cout.precision(17);
cout << "Smol: " << fixed << d << endl;

This prints

Smol: 0.00000000000000000

This is a bit too wrong for the correct answer. There is also a questionable usage of hardcoded precision. But the answer has a second, slightly different part:

#include <limits>

typedef std::numeric_limits< double > dbl;

double d = 3.14159265358979;
cout.precision(dbl::max_digits10);
cout << "Pi: " << d << endl;

This one is better: it uses max_digits10 instead of the hardcoded precision and removes fixed. Does it work with our smol number (godbolt)?

typedef std::numeric_limits< double > dbl;
cout.precision(dbl::max_digits10);
cout << "Smol: " << 1e-20 << endl;

prints

9.9999999999999995e-21

which is not pretty but not terribly wrong. If you read it back you’ll get the correct value but it’s not very user-friendly.

Does the second solution always work? Not really, because unfortunately cout uses the global state to control formatting. So it will break if the floating point format flags where earlier set to fixed (godbolt):

// Some place else:
cout << fixed << 0.42 << endl;
//  ...
typedef std::numeric_limits< double > dbl;
cout.precision(dbl::max_digits10);
cout << "Smol: " << 1e-20 << endl;

Score: 3/10 because of the completely wrong first part of the answer and subtly wrong second part and no explanation how these two relate to each other.

Let’s move to the second top answer with 90 votes:

#include <iomanip>
std::cout << std::setprecision (15) << 3.14159265358979 << std::endl;

This one is really broken because it uses insufficient precision in addition to being affected by the global state. This is easy to see by printing a more precise approximation of π (godbolt):

std::cout << std::setprecision (15) << 3.141592653589793 << std::endl;
std::cout << std::setprecision (17) << 3.141592653589793 << std::endl;

This prints:

3.14159265358979
3.1415926535897931

On a positive note it namespace qualifies names and uses a fancy std::setprecision I/O manipulator.

Score: 1/10 because it gets the most important part wrong.

The next answer with 28 votes gives us this code:

std::cout << std::setprecision (std::numeric_limits<double>::digits10 + 1)
          << 3.14159265358979
          << std::endl;

This one uses an interesting formula std::numeric_limits<double>::digits10 + 1 which feels clever. For IEEE 754 double this gives us 16 which is more precision than the second answer but less than the first answer. At this point I think people are brute forcing the correct precision although the general answer has been known at least since 1968 and is available from Wikipedia:

If an IEEE 754 double-precision number is converted to a decimal string with at least 17 significant digits, and then converted back to double-precision representation, the final result must match the original number.

This time we can have our π and print it too because it so happens that 16 digits is enough to round trip it. But not enough to round trip 1.2345678901234567 (godbolt):

std::cout << std::setprecision (std::numeric_limits<double>::digits10 + 1)
          << 1.2345678901234567
          << std::endl;

This prints

1.234567890123457

If you stare long enough you can see that it’s wrong. To convince yourself that this is not just a rounding issue you can print both 1.2345678901234567 and 1.234567890123457 with precision of 17 and see that this gives different results.

Score: 2/10 because it gets the most important part wrong but at least does it in a fancy and slightly less wrong way than the previous answer.

I’ll leave my own answer to the end so let’s look at the one with approximately the same number of votes. The first method it suggests is

std::cout.precision(std::numeric_limits<double>::max_digits10 - 1);
std::cout << std::scientific <<  1.0/7.0 << '\n';

// C++11 Typical output
1.4285714285714285e-01

This is the first correct answer which is why it only has measly 17 votes. Subtracting one looks counter-intuitive but it accounts for the fact that the scientific format only counts digits after the decimal point. It is not affected by the global state because it always sets the format. The only small drawbacks are that it may print more digits than necessary and always prints the exponent.

The answer rightfully warns against using the fixed format and also suggests hexfloat as another alternative:

std::cout << "hexfloat: " << std::hexfloat << exp (-100) << '\n';
std::cout << "hexfloat: " << std::hexfloat << exp (+100) << '\n';
// output
hexfloat: 0x1.a8c1f14e2af5dp-145
hexfloat: 0x1.3494a9b171bf5p+144

Score: 8/10 because it is gives correct but suboptimal result

The method that I’d recommend is, of course, using C++20 std::format or, if the latter is not yet available on your system, fmt::format which implements all of the standard features and more:

std::cout << std::format("{}", std::numbers::pi_v<double>);

It gives you the shortest decimal representation with the round trip guarantee and the correct rounding. It is not affected by global state and, unlike the previous answer, won’t print unnecessary digits. And you don’t need to manually specify the precision.

P.S. The question doesn’t actually define what it means by “full precision”. Normally it is understood as precision enough for round trip through decimal but there is another possible but unlikely interpretation of the maximum number of (significant) decimal digits. For IEEE 754 double it’s whopping 767 digits! It is also one of the reasons why your printf implementation uses multiprecision arithmetic.

Happy floating-point formatting!


Last modified on 2023-06-04