Home > database >  Why does this bad universal initializer syntax compile and result in unpredictable behavior?
Why does this bad universal initializer syntax compile and result in unpredictable behavior?

Time:06-02

I have a bunch of code for working with hardware (FPGA) registers, which is roughly of the form:

struct SomeRegFields {
    unsigned int lower : 16;
    unsigned int upper : 16;
};

union SomeReg {
    unsigned int wholeReg;
    SomeRegFields fields;
};

(Most of these register types are more complex. This is illustrative.)

While cleaning up a bunch of code that set up registers in the following way:

SomeReg reg1;
reg1.wholeReg = 0;
// ... assign individual fields
card->writeReg(REG1_ADDRESS, reg1.wholeReg);

SomeReg reg2;
reg2.wholeReg = card->readReg(REG2_ADDRESS);
// ... do something with reg2 field values

I got a bit absent-minded and accidentally ended up with the following:

SomeReg reg1{ reg1.wholeReg = 0 };
SomeReg reg2{ reg2.wholeReg = card->readReg(REG2_ADDRESS) };

The reg1.wholeReg = part is wrong, of course, and should be removed.

What's bugging me is that this compiles on both MSVC and GCC. I would have expected a syntax error here. Moreover, sometimes it works fine and the value actually gets copied/assigned correctly, but other times, it will result in a 0 value even if the register value returned is non-0. It's unpredictable, but appears to be consistent between runs which cases work and which don't.

Any idea why the compilers don't flag this as bad syntax, and why it seems to work in some cases but breaks in others? I assume this is undefined behavior, of course, but why would it would change behaviors between what often seem like nearly identical calls, often back-to-back?


Some compilation info:

If I run this through Compiler Explorer:

int main()
{
    SomeReg myReg { myReg.wholeReg = 10 };
    return myReg.fields.upper;
}

This is the code GCC trunk spits out for main with optimization off (-O0):

main:
    push    rbp
    mov     rbp, rsp
    mov     DWORD PTR [rbp-4], 10
*   mov     eax, DWORD PTR [rbp-4]
*   mov     DWORD PTR [rbp-4], eax
    movzx   eax, WORD PTR [rbp-2]
    movzx   eax, ax
    pop     rbp
    ret

The lines marked with * are the only difference between this version and a version without the bad myReg.wholeReg = part. MSVC gives similar results, though even with optimization off, it seems to be doing some. In this case, it just causes an extra assignment in and back out of a register, so it still works as intended, but given my accidental experimental results, it must not always compile this way in more complex cases, i.e. not assigning from a compile-time-deducible value.

CodePudding user response:

reg1.wholeReg = card->readReg(REG2_ADDRESS) 

This is simply treated as an expression. You are assigning the return value of card->readReg(REG2_ADDRESS) to reg1.wholeReg and then you use the result of this expression (a lvalue referring to reg1.wholeReg) to aggregate-initialize the first member of reg2 (i.e. reg2.wholeReg). Afterwards reg1 and reg2 should hold the same value, the return value of the function.

Syntactically the same happens in

SomeReg reg1{ reg1.wholeReg = 0 };

However, here it is technically undefined behavior since you are not allowed to access variables or class members before they are initialized. Practically speaking, I would expect this to usually work nontheless, initializing reg1.wholeReg to 0 and then once again.

Referring to a variable in its own initializer is syntactically correct and may sometimes be useful (e.g. to pass a pointer to the variable itself). This is why there is no compilation error.


int main()
{
    SomeReg myReg { myReg.wholeReg = 10 };
    return myReg.fields.upper;
}

This has additional undefined behavior, even if you fix the initialization, because you can't use a union in C for type punning at all. That is always undefined behavior, although some compilers might allow it to the degree that is allowed in C. Still, the standard does not allow reading fields.upper if wholeReg is the active member of the union (meaning the last member to which a value was assigned).

  • Related