What's the best way to serialize a double string and back again?
I have a Java program that starts a C ++ program from ProcessBuilder
and reads the output from a C ++ application as a string. Data is a collection of data types where I serialize everything as a string through std::cout
, for example:
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 am doing something like:
String line = readLineFromSubProcess();
String[] parts = line.split(":");
if (parts[0].equals("double")) {
return Double.parseDouble(parts[1]);
}
However, since I am reading sensor data, I am concerned about the loss of precision in floating point values. I have direct control over the serialization format.
How do I write a value double
from C ++ to std::cout
such that reading it from Java produces exactly the same double value?
The solution I decided to go with is reinterpret_cast
double
on int64_t
and float
on int32_t
, and with partial custom templating for just these two data types. I'm still missing the endianess conversion, but since everything is small here, I think everything will 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 don't literally do reinterpret_cast
, since my compiler (GCC) is giving me an alias warning which is related to compiler switches, re using here:
a dereferenced type-guard pointer would violate strict antialiasing rules
On the Java side I am 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]));
}
What is Google Protocol Buffers work under the hood .
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);
}
So the printed double letters will be anti-aliased using full precision (17). I recommend using 17 as the default will always try to round. In any case, full correct printing using this method would require a more powerful library.
Method 2: (binary)
Just memcpy value using and int64 is ok, double can store maximum 53 bits:
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;
}
Did it help?
std::cout << std::setprecision(5) << f << '\n';
Please see examples:
http://www.cplusplus.com/reference/iomanip/setprecision/