Store Float16 variables I'm currently rolling a little Float16 implementation. It's minimalistic and can't handle infinity, NaN and will neither work properly with overflows nor with underflows. (That's not the goal btw.)

Is there something particular wrong with the following implementation which could lead to major issues?

class Float16
{
public:
    Float16(float other)
    {
        IEEESingle f32;
        f32.Float = other;

        if (other == 0.0f)
        {
            bits.IEEE.Exp = 0;
            bits.IEEE.Man = 0;

            return;
        }

        bits.IEEE.Sign = f32.IEEE.Sign;
        bits.IEEE.Exp  = f32.IEEE.Exp - 112;
        bits.IEEE.Man  = f32.IEEE.Man &gt;&gt; 13;
    }

    operator float() const
    {
        if (bits.IEEE.Exp == 0 &amp;&amp; bits.IEEE.Man == 0)
        {
            return 0.0f;
        }

        IEEESingle f32;

        //112--&gt;F32 Exponent = N + 127, F16 Exponent = N + 15; 

        f32.IEEE.Sign = bits.IEEE.Sign;
        f32.IEEE.Exp  = bits.IEEE.Exp + 112; 
        f32.IEEE.Man  = bits.IEEE.Man &lt;&lt; 13;

        //&lt;&lt; 13 since Mantisse f32 = 23 and Mantisse F16 = 10

        return f32.Float;
    }

private:
    union IEEEHalf
    {
        uint16_t bits;
        struct
        {
            uint16_t Man  : 10;
            uint16_t Exp  : 5;
            uint16_t Sign : 1;
        } IEEE;
    } bits;

    union IEEESingle
    {
        float Float;
        struct
        {
            uint32_t Man  : 23;
            uint32_t Exp  : 8;
            uint32_t Sign : 1;
        } IEEE;
    };
};

int main()
{
    Float16 f16 = 1.5f;
    float   f32 = (float)f16;

    return 0;
}


I am aware that my calculations will happen at F32 level and I don't intend to implement direct F16 calculations. I am planning to add some operators to the class, but the question focuses mostly on the conversion between f16 and f32. I am aware of the precision and rounding issues of Float16 values.
Is there something particular wrong with the following implementation which could lead to major issues?


Yes; the union seems to be used in a way that is actually Undefined Behavior.  Let’s see…

    f32.IEEE.Sign = bits.IEEE.Sign;
    f32.IEEE.Exp  = bits.IEEE.Exp + 112; 
    f32.IEEE.Man  = bits.IEEE.Man &lt;&lt; 13;

    return f32.Float;


Yes, you are reading from the Float member that is never written to, the optimizer can know that it is not the active member, and act as if the program never reaches this point.  Anything is possible, including crashing.

We have seen the optimizers get more and more aggressive, taking advantage of U.B., breaking “bad” code that worked historically.  So, it is a real concern.  In any case, the layout of the bit fields is up to the implementation; that is only an issue if you plan on saving these 16-bit values to files.

See Scott Schurr’s presentation on this.  Your specific case is handled toward the end, and he shows that of all the work-arounds, using intrinsic memcpy with the items known to be properly aligned will be completely optimized out by the compiler, generating the code you really intended with the union!



See also ⧺ES.9.



float   f32 = (float)f16;


Don’t use C-style casts.  Any appearance of such a cast is a flag for code review; casts that are actually needed should be function style, specialized, or use clearly named helper functions.

You supplied an implicit conversion, so you don’t need this cast at all.