Are there any guarantees for unions that contain a

2019-01-26 06:28发布

问题:

Can I put a T and a wrapped T in an union and inspect them as I like?

union Example {
    T value;
    struct Wrapped { 
       T wrapped;
    } wrapper;
};
// for simplicity T = int

Example ex;
ex.value = 12;
cout << ex.wrapper.wrapped; // ?

The C++11 standards only guarantee save inspection of the common initial sequence, but value isn't a struct. I guess the answer is no, since wrapped types aren't even guaranteed to be memory compatible to their unwrapped counterpart and accessing inactive members is only well-defined on common initial sequences.

回答1:

I believe this is undefined behavior.

[class.mem] gives us:

The common initial sequence of two standard-layout struct types is the longest sequence of non-static data members and bit-fields in declaration order, starting with the first such entity in each of the structs, such that corresponding entities have layout-compatible types and either neither entity is a bit-field or both are bit-fields with the same width. [...]

In a standard-layout union with an active member of struct type T1, it is permitted to read a non-static data member m of another union member of struct type T2 provided m is part of the common initial sequence of T1 and T2; the behavior is as if the corresponding member of T1 were nominated.

If T isn't a standard layout struct type, this is clearly undefined behavior. (Note that int is not a standard layout struct type, as it's not a class type at all).

But even for standard layout struct types, what constitutes a "common initial sequence" is based strictly on non-static data members. That is, T and struct { T val; } do not have a common initial sequence - there are no data members in common at all!

Hence, here:

template <typename T>
union Example {
    T value;
    struct Wrapped { 
       T wrapped;
    } wrapper;
};


Example<int> ex;
ex.value = 12;
cout << ex.wrapper.wrapped; // (*)

you're accessing an inactive member of the union. That's undefined.



回答2:

Union behavior is undefined when accessing a member that wasn't the last one written to. So no, you can't depend on this behavior.

It's identical in principle to the idea of having a union to extract specific bytes from an integer; but with additional risk of the fact that you're now depending on the compiler not adding any padding in your struct. See Accessing inactive union member and undefined behavior? for more details.



回答3:

It should work because both Example and Wrapped are standard layout classes, and C++14 standard has enough requirements to guarantee that in that case value and wrapper.wrapped are located at the same address. Draft n4296 says in 9.2 Class members [class.mem] §20:

If a standard-layout class object has any non-static data members, its address is the same as the address of its first non-static data member.

A note even says:

[ Note: There might therefore be unnamed padding within a standard-layout struct object, but not at its beginning, as necessary to achieve appropriate alignment. —end note ]

That means that you at least respect the strict aliasing rule from 3.10 Lvalues and rvalues [basic.lval] §10

If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined
— the dynamic type of the object,
...
— an aggregate or union type that includes one of the aforementioned types among its elements or nonstatic data members (including, recursively, an element or non-static data member of a subaggregate or contained union),

So this is perfectly defined:

cout << *(&ex.wrapper.wrapped) << endl

because &ex.wrapper.wrapped is required to be the same as &ex.value and the pointed object has the correct type. . But as the standard is explicit only for common subsequence. So my understanding is cout << ex.wrapper.wrapped << endl invokes undefined behaviour, because of a note in 1.3.24 [defns.undefined] about undefined behavior says (emphasize mine):

Undefined behavior may be expected when this International Standard omits any explicit definition of behavior...

TL/DR: I would bet a coin that most if not all common implementation will accept it, but because of the note from 1.3.24 [defns.undefined], I would never use that in production code but would use *(&ex.wrapper.wrapped) instead.


In the more recent draft n4659 for C++17, the relevant notion is inter-convertibility ([basic.compound] §4).