3

I have a Java program that launches a C++ program with ProcessBuilder, and reads the output from the C++ application as an string. The data is a mix of datatypes, where I'm serializing everything as an string via std::cout, like this:

void write_data(int x) {
    std::cout << "int:" << x << std::endl;
}

void write_data(double x) {
    std::cout << "double:" << x << std::endl;
}

On the Java side I'm doing something like:

String line = readLineFromSubProcess();
String[] parts = line.split(":");

if (parts[0].equals("double")) {
  return Double.parseDouble(parts[1]);
}

However, since I'm reading sensors data I'm concerned about the loss of precision on the floating point values. I'm in direct control on the serialization format.

How can I write a double value from C++ to std::cout in a way that reading it from Java produces exactly the same double value?

vz0
  • 32,345
  • 7
  • 44
  • 77
  • Is there any way to get the sensor data out in an integral fashion? Else serialise it with the precision (or accuracy) that the manufacturer of the sensor specifies it has. – Niall Oct 14 '14 at 10:23
  • The data comes from a custom in-house database, there are some apps that write doubles to this database, and I need to read them back to send to a 3rd party monitoring Java program. All I have is this double value. So far I've been using the naive `std::cout << x` approach since its easy to read on the screen for us. – vz0 Oct 14 '14 at 10:26
  • Given that they come from a serialised source, when you read them in store the precision they come with and then write them out again with that same precision. – Niall Oct 14 '14 at 10:28
  • @Niall I don't know what precision those values were written with, and I don't think there is someone around here who can even answer that question... Since a double is an IEEE 754 I was thinking more on the lines of serializing the raw bytes, reading them from Java and building the original bits again. – vz0 Oct 14 '14 at 10:32
  • You could read your doubles in java using c++ with jni interface, do some preprocessing and return to java frontend. If you are concerned about formatting problems - loosing precision, then maybe write to cout in binary format - for example uuencode binary double data. But I would do some heavy testing, to find examples of data with lost precision. – marcinj Oct 14 '14 at 10:34
  • It would take some testing (and may be implementation specific, endianess issues etc.), but the serialised raw bytes could work. – Niall Oct 14 '14 at 10:46
  • Maybe write the ieee representation to binary and read that? See http://stackoverflow.com/questions/4733147/portability-of-binary-serialization-of-double-float-type-in-c – Erik Alapää Oct 14 '14 at 10:46
  • just to understand - you are using c++ code to read a database and then a java layer on top of this to propagate to the 3rd party app? – Nim Oct 14 '14 at 12:24
  • erm - why not use java to talk to the database and forgo the c++ layer? – Nim Oct 14 '14 at 13:54
  • @Nim It's a DB some colleagues made, and we don't have Java bindings, just some adhoc C++ API. – vz0 Oct 14 '14 at 13:58
  • ah okay, you could always do the hacky convert to hex string, then use `ByteBuffer` to rebuild from hex... still will need to handle endianess though... – Nim Oct 14 '14 at 14:03
  • I've added an answer. – vz0 Oct 15 '14 at 11:49

3 Answers3

2

The solution I've decided to go with is to reinterpret_cast the double to an int64_t and the float to an int32_t, and using partial template specialization just for this two data types. I'm still missing the endianess conversion but since everything here is little endian I think I'll be fine:

template <typename T>
void write_data(T x) {
    std::cout << get_type_name<T>() << ":" << x << std::endl;
}

template<>
void write_data<double>(double x) {
    union {
        double d;
        int64_t i;
    } n;
    n.d = x;
    std::cout << "double_as_int64:" << n.i << std::endl; 
}

template<>
void write_data<float>(double x) {
    union {
        float f;
        int32_t i;
    } n;
    n.f = x;
    std::cout << "float_as_int32:" << n.i << std::endl; 
}

I'm not literally doing a reinterpret_cast since my compiler (GCC) gives me a warning about aliasing, which has to do with the compiler switches we're using here:

dereferencing type-punned pointer will break strict-aliasing rules

On the Java side I'm doing:

String line = readLineFromSubProcess();
String[] parts = line.split(":");

if (parts[0].equals("double")) {
  return Double.parseDouble(parts[1]);
} else if (parts[0].equals("double_as_int64") {
  return Double.longBitsToDouble(Long.parseLong(parts[1]));
} else if (parts[0].equals("float_as_int32") {
  return Float.intBitsToFloat(Integer.parseInt(parts[1]));
}

Which is what Google Protocol Buffers are doing under the hood.

Community
  • 1
  • 1
vz0
  • 32,345
  • 7
  • 44
  • 77
  • Warning: While this trick will usually work, it is undefined behavior in C++ to store in a union as one type and read from it as another. This technique is correct in C, but in C++ it could bite you, most likely due to compiler optimizations that make incorrect assumptions. – Charles Ofria Jan 03 '22 at 16:44
1

Well, as you said, there are two options, if you care about readability and need to print doubles then:

Method 1: (readable)

#include <limits>

void double_to_string(double the_double, int offset_to_max)
{
        typedef std::numeric_limits<double> doublelimit;

        // ready to transfer
        std::cout.precision(doublelimit::digits10 + offset_to_max);
        std::cout << the_double << std::endl;
}

// Back to double
double cstring_to_double(const char *s)
{
        return std::atof(s);
}

int main(int argc, char *argv[]) {

        double the_double = 0.10000000000000002;
        //double the_double = 0.1; 
        int offset = 2;

        double_to_string(the_double, offset);
}

This way, doubles printed will have aliasing using full precision (17). I recomend using 17 since the default will always try to round. Either way, a full correct print using this method will require a more powerful library.

Method 2: (binary)

Just memcpy the value, using and int64 is fine, double can store 53 bits max:

template<typename T, typename H = int64_t>
H write_data(T x) 
{
    H i;
    // Can we hold the data?
    if (sizeof(T) > sizeof(H)) assert(false); // or write some error procesing
    // Now take the lower size of the two.
    memcpy(&i, &x, sizeof(H) > sizeof(T) ? sizeof(T) : sizeof(H));

    return i;
}

// union version
template<typename T, typename H = int64_t>
H write_data_u(T x)
{
    // Can we hold the data?
    if (sizeof(T) > sizeof(H)) assert(false); // or write some error procesing

    union {
        T d;
        H i;
    } n;

    n.d = x;

    return n.i;
}


int main(int argc, char *argv[]) 
{
    double the_double = 0.10000000000000001;
    int64_t double_holder = write_data<double, int64_t>(the_double);

    // Binary data as decimal string: ready to transfer the string, remember to convert to big_endian first if you transfer the binary representation.
    std::cout << "double_as_int64:" << double_holder << std::endl;

    // Back to double (we know int64_t holds a double so we dont care about the remaining bits)
    double back_to_double = write_data<int64_t, double>(double_holder);
    std::cout.precision(std::numeric_limits<double>::digits10 + 2);
    std::cout << "back_to_double:" << back_to_double << std::endl;
}
Tek
  • 46
  • 1
  • 4
-1

Will this help?

std::cout << std::setprecision(5) << f << '\n';

Please see the examples at:

http://www.cplusplus.com/reference/iomanip/setprecision/

blalasaadri
  • 5,990
  • 5
  • 38
  • 58