1

Here's my attempt:

#include <iostream>

union newType {
        long double firstPart;
        unsigned char secondPart[sizeof(firstPart)];
} lDouble;

int main() {
    lDouble.firstPart = -16.5;

    for (int_fast16_t i { sizeof(lDouble) - 1 }; i >= 0; --i)
        std::cout << (int)lDouble.secondPart[i] << " ";

    return 0;
}
Output:  0 0 0 0 0 0 192 3 132 0 0 0 0 0 0 0  
Hex:     0 0 0 0 0 0  c0 3  84 0 0 0 0 0 0 0

And I almost agree with the part "c0 3 84", which is "1100 0000 0000 0011 1000 0100".

-16.5 = -1.03125 * 2^4 = (-1 + (-0.5) * 2^-4) * 2^4
Thus, the 117th bit of my fraction part must be 1 and after 5th division I'll get only "0".

sign(-):       1  
exponent(2^4): 4 + 16383 = 16387 = 100 0000 0000 0011  
fraction:      0000 1000 and 104 '0'

Result:   1| 100 0000 0000 0011| 0000 1000 and 104 '0'
Hex:           c    0    0    3     0    8 and 26 '0'

Or: c0 3 8 0 0 0 0 0 0 0 0 0 0 0 0 0

I don' get two things:

  1. "c0 3 84" - where did I lose 4 in my calculations? My guess is that it somehow stores 1 (113 bit) and it shouldn't be stored. Then there's 1000 0100 instead of 0000 1000 (after "c0 3") and that's exactly "84". But we always store 112 bits and 1 is always implicit.
  2. Why doesn't my output start from 192? Why does it start from 0? I thought that first bit is sign bit, then exponent (15 bits) and fraction (112 bits).

I've managed to represent other data types (double, float, unsigned char, etc.). With double I went with the similar approach and got the expected result (e.g. double -16.5 outputs 192 48 128 0 0 0 0 0, or c0 30 80 0 0 0 0 0).

Of course I've tested the solution from How to print binary representation of a long double as in computer memory?

Values for my -16.5 are: 0 0 0 0 0 0 0 0x84 0x3 0xc0 0xe2 0x71 0xf 0x56 0 0  
If I revert this I get:  0 0 56 f 71 e2 c0 3 84 0 0 0 0 0 0 0

And I don't understand why (again) does the sequence start not from sign bit, what are those "56 f 71 e2 c0"? Where do they come from? And why (again) there's "4" after "8"?

Xofrio
  • 15
  • 2
  • The size and binary representation depends on your system. It's not required to have 80 bits or use IEEE-754. [cppreference](https://en.cppreference.com/w/cpp/language/types) _"**long double** - extended precision floating point type. Does not necessarily map to types mandated by IEEE-754. Usually 80-bit x87 floating point type on x86 and x86-64 architectures."_ –  Apr 21 '21 at 15:23
  • I didn't complain about required size. `sizeof(long double)` gives me 16 (128 bits) and I thought that my array should be filled from left-most bit, that's all. So my system uses IEEE-754 for double and for long double it uses something else? That's strange, I'll try it on my Linux machine then. Many thanks, @jabaa. – Xofrio Apr 21 '21 at 15:36
  • @Xofrio it still uses IEEE-754 for `long double` but **not** binary128. That's the [binary64 extended format](https://stackoverflow.com/a/67149953/995714). See also [Is long double in C++ an implementation of IEEE's binary128?](https://stackoverflow.com/q/52762881/995714), [What is long double on x86-64?](https://stackoverflow.com/q/15176290/995714), [Why does a long double take up only 10 bytes in a string?](https://stackoverflow.com/q/67045315/995714) – phuclv Apr 21 '21 at 15:42

1 Answers1

2

What is the correct way to get the binary representation of long double?

Same as the way of getting the binary representation of any trivial type. Reinterpreting as an array of unsigned char, and iterating each byte is typical and well defined solution.

std::bitset helps with the binary representation:

long double ld = -16.5;
unsigned char* it = reinterpret_cast<unsigned char*>(&ld);
for (std::size_t i = 0; i < sizeof(ld); i++) {
    std::cout
        << "byte "
        << i
        << '\t'
        << std::bitset<CHAR_BIT>(it[i])
        << '\t'
        << std::hex << int(it[i])
        << '\t'
        << std::dec << int(it[i])
        << '\n';
}

Example output on some system:

byte 0  00000000    0   0
byte 1  00000000    0   0
byte 2  00000000    0   0
byte 3  00000000    0   0
byte 4  00000000    0   0
byte 5  00000000    0   0
byte 6  00000000    0   0
byte 7  10000100    84  132
byte 8  00000011    3   3
byte 9  11000000    c0  192
byte 10 01000000    40  64
byte 11 00000000    0   0
byte 12 00000000    0   0
byte 13 00000000    0   0
byte 14 00000000    0   0
byte 15 00000000    0   0

Note that your example has undefined behaviour in C++ due to reading an inactive member of a union.


Why doesn't my output start from 192?

Probably because those bytes at the end happen to be padding.

Why does it start from 0?

Because the padding contains garbage.

I thought that first bit is sign bit, then exponent (15 bits) and fraction (112 bits).

Not so much the "first" bit, but rather the "most significant" bit, excluding the padding. And evidently, you've assumed the number of bits wrongly as some of it is used for padding.

Note that C++ doesn't guarantee that the floating point representation is IEEE-754 and in fact, long double is often not the 128 bit "quadruple" precision float, but rather 80 bit "extended" precision float. This is the case for example in the x86 CPU architecture family.

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • Why in your example we also start from 0, not from 40 or c0 (or 84). Is it because of those "padding bytes"? And how much exactly bytes are used for that? Does it depend from my system/compiler? – Xofrio Apr 21 '21 at 16:35
  • @Xofrio The format of all floating point types depend on the system. Padding is just unused memory. It can have any values at any time and those values have no effect on the value of the object. – eerorika Apr 21 '21 at 16:36
  • So "byte 10" in your example stores random value, am I correct? – Xofrio Apr 21 '21 at 16:39
  • @Xofrio Sort of, depending on what you mean by "random". You could get the same value if you run the program a again. Or you could get another value. Same goes for the other padding bytes. An to the bytes in your example. – eerorika Apr 21 '21 at 16:42
  • I just wanted to know where do they start (just to be clear: from byte 10, if it's "extended" precision float, right?). Sorry for so many trivial questions. – Xofrio Apr 21 '21 at 16:47
  • 1
    @Xofrio In the 80 bit precision, on CPU with 8 bit bytes, the first 10 bytes i.e. 0...9 have meaningful values and the rest 10...15 are padding. – eerorika Apr 21 '21 at 16:48
  • And the last one, if you don't mind: why there's 84 instead of 8/80 in byte 7? – Xofrio Apr 21 '21 at 17:12
  • 1
    “Reinterpreting as an array of `unsigned char`” is unclear phrasing. In C, storing in a `long double` member and reading from a member that is an array of `unsigned char` reinterprets the bytes, so one might take that phrasing to mean that. In C++, that is undefined. It would be better to explicitly state what is defined. – Eric Postpischil Apr 21 '21 at 17:18
  • Oh, I finally got it! Bit 63 is "integer part" in this "extended" precision. "Bit 63 will be 1 on all normalized numbers"! – Xofrio Apr 21 '21 at 17:48