1

I am trying to solve the relatively simple problem of being able to write a double to a file and then to read the file into a double again. Based on this answer I decided to use the human readable format.

I have successfully circumvented the problems some compilers have with nan and [-]infinity according to this question. With finite numbers I use the std::stod function to convert the string representation of a number into the number itself. But from time to time the parsing fails with numbers close to zero, such as in the following example:

#include <cmath>
#include <iostream>
#include <sstream>
#include <limits>

const std::size_t maxPrecision = std::numeric_limits<double>::digits;
const double small = std::exp(-730.0);

int main()
{
    std::stringstream stream;
    stream.precision(maxPrecision);
    stream << small;
    std::cout << "serialized:    " << stream.str() << std::endl;
    double out = std::stod(stream.str());
    std::cout << "de-serialized: " << out << std::endl;
    return 0;
}

On my machine the result is:

serialized:     9.2263152681638151025201733115952403273156653201666065e-318
terminate called after throwing an instance of 'std::out_of_range'
  what():  stod
The program has unexpectedly finished.

That is, the number is too close to zero to be properly parsed. At first I thougth that the problem is that this number is denormal, but this doesn't seem to be the case, since the mantissa starts with a 9 and not a 0.

Qt on the other hand has no problems with this number:

#include <cmath>
#include <limits>

#include <QString>
#include <QTextStream>

const std::size_t maxPrecision = std::numeric_limits<double>::digits;
const double small = std::exp(-730.0);

int main()
{
    QString string = QString::number(small, 'g', maxPrecision);
    QTextStream stream(stdout);
    stream.setRealNumberPrecision(maxPrecision);
    stream << "serialized:    " << string << '\n';
    bool ok;
    double out = string.toDouble(&ok);
    stream <<  "de-serialized: " << out << '\n' << (ok?"ok":"not ok") << '\n';
    return 0;
}

Outputs:

serialized:    9.2263152681638151025201733115952403273156653201666065e-318
de-serialized: 9.2263152681638151025201733115952403273156653201666065e-318
ok

Summary:

  1. Is this a bug in the gcc implementation of standard library?
  2. Can I circumvent this elegantly?
  3. Should I just use Qt?
Community
  • 1
  • 1
Martin Drozdik
  • 12,742
  • 22
  • 81
  • 146
  • 2
    The hex format is there for roundtrip conversion. Last time I checked (last year?) both g++ and msvc were a bit deficient in support of that. But it's not difficult to cajole them into cooperating. – Cheers and hth. - Alf Nov 26 '14 at 12:30
  • 5
    Answering question #2: This is probably my "C-way" kind of thinking, but you could copy the `double` into a `uint64` (mem-copying, not type-casting), serialize the `uint64` instead, then do the opposite on de-serialization. – barak manos Nov 26 '14 at 12:34
  • @barakmanos Thank you! But wouldn't that be the same as using a binary format? I prefer to use the human readable one. – Martin Drozdik Nov 26 '14 at 12:38
  • You'll serialize it into a different (yet readable) value. BTW, implementation-wise, perhaps you should serialize it into an array of `unsigned char` values instead, in order to avoid breaking strict-aliasing rules. – barak manos Nov 26 '14 at 12:42
  • @barakmanos What do you mean by aliasing rules? Could you give me some references? I came across this approach in this answer http://stackoverflow.com/a/4733588/1097451, but their approach does not handle infinities and nan. – Martin Drozdik Nov 26 '14 at 12:48
  • @Cheersandhth.-Alf Thank you! So, does this classify as a bug? – Martin Drozdik Nov 26 '14 at 13:26
  • 1
    @MartinDrozdik: *Probably*. There's a big nest of bugs somewhere nearby this functionality. I've posted a new question, http://stackoverflow.com/questions/27161720/fix-for-bizarre-a-format-behavior-with-g-4-9-1 – Cheers and hth. - Alf Nov 27 '14 at 01:47

2 Answers2

4

Answering question #2:

This is probably my "C-way" kind of thinking, but you could copy the double into a uint64_t (mem-copying, not type-casting), serialize the uint64_t instead, and do the opposite on de-serialization.

Here is an example (without even having to copy from double into uint64_t and vice-versa):

uint64_t* pi = (uint64_t*)&small;
stringstream stream;
stream.precision(maxPrecision);
stream << *pi;
cout << "serialized:    " << stream.str() << endl;
uint64_t out = stoull(stream.str());
double* pf = (double*)&out;
cout << "de-serialized: " << *pf << endl;

Please note that in order to avoid breaking strict-aliasing rule, you actually do need to copy it first, because the standard does not impose the allocation of double and uint64_t to the same address-alignment:

uint64_t ismall;
memcpy((void*)&ismall,(void*)&small,sizeof(small));
stringstream stream;
stream.precision(maxPrecision);
stream << ismall;
cout << "serialized:    " << stream.str() << endl;
ismall = stoull(stream.str());
double fsmall;
memcpy((void*)&fsmall,(void*)&ismall,sizeof(small));
cout << "de-serialized: " << fsmall << endl;
barak manos
  • 29,648
  • 10
  • 62
  • 114
  • Great! This seems to handle also infinities and nan. – Martin Drozdik Nov 26 '14 at 13:23
  • @MartinDrozdik: Yep, I was just about to add this notion in response to your comment at the question. Since all possible `uint64_t` values are "normal", there should be no issue with either `inf` or `NaN`. – barak manos Nov 26 '14 at 13:27
  • @MartinDrozdik: And again, at least on the theoretical aspect, you should mem-copy it to `uint8_t arr[sizeof(small)]`, because there is no guarantee by the standard that the compiler must allocate `uint64_t` and `double` with the same (64-bit) address-alignment. – barak manos Nov 26 '14 at 13:30
  • For completeness, is this answer portable across systems of different architectures (e.g. ARM to x86)? – Xofo Aug 07 '19 at 16:51
  • While this answer works, it's extremely wasteful and shouldn't be used in systems where space and performance matter. The other answer with `frexp` is the good one. – The Quantum Physicist Jul 12 '21 at 07:35
3

If you're open to other recording methods you can use frexp:

#include <cmath>
#include <iostream>
#include <sstream>
#include <limits>


const std::size_t maxPrecision = std::numeric_limits<double>::digits;
const double small = std::exp(-730.0);

int main()
{
    std::stringstream stream;
    stream.precision(maxPrecision);

    int exp;
    double x = frexp(small, &exp);

    //std::cout << x << " * 2 ^ " << exp << std::endl;
    stream << x << " * 2 ^ " << exp;

    int outexp;
    double outx;

    stream.seekg(0);

    stream >> outx;
    stream.ignore(7); // >> " * 2 ^ "
    stream >> outexp;

    //std::cout << outx << " * 2 ^ " << outexp << std::endl;

    std::cout << small << std::endl << outx * pow(2, outexp) << std::endl;

    return 0;
}
Jonathan Mee
  • 37,899
  • 23
  • 129
  • 288