1

Type-punning through unions has defined behaviour in C:

typedef union
{
    uint32_t i;
    float f;
} MyUnion;

MyUnion u;
u.f = 4.2;
print("%"PRIu32, u.i); // defined behaviour

Given via a footnote in the C Standard:

If the member used to read the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called “type punning”). This might be a trap representation.


However, is it still defined behaviour when atomic operations are used? And what is that behaviour? Do we have some of the sequencing guarantees?

typedef union
{
    _Atomic uint32_t i;
    _Atomic float f; // assume sizeof(float) == sizeof(uint32_t)
} MyUnion;

int load(MyUnion* p)
{
    atomic_load(&p->i);
}

void store(MyUnion* p)
{
    return atomic_store(&p->i, 42);
}

You can also construct strange examples like:

typedef union
{
    _Atomic uint32_t i;
    struct
    {
        uint16_t h0; // or maybe _Atomic uint16_t ah0;
        _Atomic uint16_t ah1;
    };
} MyUnion;

6.5.2.3p5 at least says:

Accessing a member of an atomic structure or union object results in undefined behavior.

typedef _Atomic union
{
    uint32_t i;
    float f;
} MyUnion;

int load(MyUnion* p)
{
    return atomic_load(&p->i); // UB, also atomic_load on non-atomic type
    return p->i; // UB
}
dyp
  • 38,334
  • 13
  • 112
  • 177
  • While the type punning itself is well-defined, it's nonsense to re-interpret a float as an int and vice versa, regardless of `_Atomic`. The conversion might raise floating point exceptions for example. – Lundin Apr 06 '22 at 12:57
  • I don't know a formal answer here, but I strongly suspect your code is not well defined. Anyway, I'd think it would be simpler and safer to just use a single type for your `_Atomic` object, say `int`, and then code that wants to pun it as `float` should just load the `int` value and run it through its own private `union`. Vice versa for a store. – Nate Eldredge Apr 06 '22 at 14:39
  • It does mean that you won't get access to atomic arithmetic operators on `float`, e.g. `fetch_add`. But those are usually implemented as a CAS loop anyway, and so you could just write it yourself with `atomic_compare_exchange_weak` on the `int`. – Nate Eldredge Apr 06 '22 at 14:41
  • @Lundin: Not "nonsense", surely. It's implementation-defined, but most implementations define their representations of `int` and `float` very carefully, and so you often see this construct in code meant for particular implementations. If you construct your `int` value correctly, you can be sure of what you will get, and that you won't get any sort of trap or exception. But surely you know that? – Nate Eldredge Apr 06 '22 at 14:44
  • There would most certainly be data race problems if either `int` or `float` were not lock-free, or if they were of different sizes. So I find it unlikely that C guarantees anything at all in general about atomicity here. – Nate Eldredge Apr 06 '22 at 14:46
  • @NateEldredge Using _signed_ `int` to express the raw binary representation of something is always nonsense. For such purposes you would use `uint32_t`/`unsigned int`. Signed floats do not "sign extend" in the same manner as 2's complement `int`, so all manner of subtle bugs are possible as soon as you involve bitwise operators or other arithmetic. – Lundin Apr 06 '22 at 14:58
  • ... and I was just trying to make the example a bit simpler by using `int` instead of `uint32_t`. I know it doesn't make a lot of sense, the code which I encountered "in the wild" was more like the union of `uint32_t` and `uint16_t`. – dyp Apr 06 '22 at 15:31

1 Answers1

0

is it still defined behaviour when atomic operations are used?

The same conditions apply. It may be a trap representation, and _Atomic objects may differ in size, representation, alignment compared to the unqualified types. If it is not a trap representation, then the result will be (implementation-) defined.

KamilCuk
  • 120,984
  • 8
  • 59
  • 111
  • But do the synchronization guarantees on atomics apply? E.g. a load-acquire on the float and a store-release on the int. – dyp Apr 06 '22 at 14:33
  • 1
    And that is the real question ;) . I believe this will be either under-specified and undefined behavior by omission, or just undefined, (or implementation defined at best). I do not believe it would be sane to expect synchronization on type-punned atomics. _Atomic (theoretically) may have some additional data and hidden fields, so `union { _Atomic char c; _Atomic int i; }` and accessing both of them at the same time will just be trouble. – KamilCuk Apr 06 '22 at 15:21