Union 'Punning' Structs W/ "Common Initial Sequence": Why Does C (99+), But Not C++, Stipulate a 'Visible Declaration of the Union Type'

union 'punning' structs w/ common initial sequence : Why does C (99+), but not C++, stipulate a 'visible declaration of the union type'?

I've found my way through the labyrinth to some great sources on this, and I think I've got a pretty comprehensive summary of it. I'm posting this as an answer because it seems to explain both the (IMO very misguided) intention of the C clause and the fact that C++ does not inherit it. This will evolve over time if I discover further supporting material or the situation changes.

This is my first time trying to sum up a very complex situation, which seems ill-defined even to many language architects, so I'll welcome clarifications/suggestions on how to improve this answer - or simply a better answer if anyone has one.

Finally, some concrete commentary

Through vaguely related threads, I found the following answer by @tab - and much appreciated the contained links to (illuminating, if not conclusive) GCC and Working Group defect reports: answer by tab on StackOverflow

The GCC link contains some interesting discussion and reveals a sizeable amount of confusion and conflicting interpretations on part of the Committee and compiler vendors - surrounding the subject of union member structs, punning, and aliasing in both C and C++.

At the end of that, we're linked to the main event - another BugZilla thread, Bug 65892, containing an extremely useful discussion. In particular, we find our way to the first of two pivotal documents:

Origin of the added line in C99

C proposal N685 is the origin of the added clause regarding visibility of a union type declaration. Through what some claim (see GCC thread #2) is a total misinterpretation of the "common initial sequence" allowance, N685 was indeed intended to allow relaxation of aliasing rules for "common initial sequence" structs within a TU aware of some union containing instances of said struct types, as we can see from this quote:

The proposed solution is to require that a union declaration be visible
if aliases through a common initial sequence (like the above) are possible.
Therefore the following TU provides this kind of aliasing if desired:

union utag {
struct tag1 { int m1; double d2; } st1;
struct tag2 { int m1; char c2; } st2;
};

int similar_func(struct tag1 *pst2, struct tag2 *pst3) {
pst2->m1 = 2;
pst3->m1 = 0; /* might be an alias for pst2->m1 */
return pst2->m1;
}

Judging by the GCC discussion and comments below such as @ecatmur's, this proposal - which seems to mandate speculatively allowing aliasing for any struct type that has some instance within some union visible to this TU - seems to have received great derision and rarely been implemented.

It's obvious how difficult it would be to satisfy this interpretation of the added clause without totally crippling many optimisations - for little benefit, as few coders would want this guarantee, and those who do can just turn on fno-strict-aliasing (which IMO indicates larger problems). If implemented, this allowance is more likely to catch people out and spuriously interact with other declarations of unions, than to be useful.

Omission of the line from C++

Following on from this and a comment I made elsewhere, @Potatoswatter in this answer here on SO states that:

The visibility part was purposely omitted from C++ because it's widely considered to be ludicrous and unimplementable.

In other words, it looks like C++ deliberately avoided adopting this added clause, likely due to its widely pereceived absurdity. On asking for an "on the record" citation of this, Potatoswatter provided the following key info about the thread's participants:

The folks in that discussion are essentially "on the record" there. Andrew Pinski is a hardcore GCC backend guy. Martin Sebor is an active C committee member. Jonathan Wakely is an active C++ committee member and language/library implementer. That page is more authoritative, clear, and complete than anything I could write.

Potatoswatter, in the same SO thread linked above, concludes that C++ deliberately excluded this line, leaving no special treatment (or, at best, implementation-defined treatment) for pointers into the common initial sequence. Whether their treatment will in future be specifically defined, versus any other pointers, remains to be seen; compare to my final section below about C. At present, though, it is not (and again, IMO, this is good).

What does this mean for C++ and practical C implementations?

So, with the nefarious line from N685... 'cast aside'... we're back to assuming pointers into the common initial sequence are not special in terms of aliasing. Still. it's worth confirming what this paragraph in C++ means without it. Well, the 2nd GCC thread above links to another gem:

C++ defect 1719. This proposal has reached DRWP status: "A DR issue whose resolution is reflected in the current Working Paper. The Working Paper is a draft for a future version of the Standard" - cite. This is either post C++14 or at least after the final draft I have here (N3797) - and puts forward a significant, and in my opinion illuminating, rewrite of this paragraph's wording, as follows. I'm bolding what I consider to be the important changes, and {these comments} are mine:

In a standard-layout union with an active member {"active" indicates a union instance, not just type} (9.5 [class.union])
of struct type T1, it is permitted to read {formerly "inspect"} 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. [Note: Reading a volatile object
through a non-volatile glvalue has undefined behavior (7.1.6.1
[dcl.type.cv]). —end note]

This seems to clarify the meaning of the old wording: to me, it says that any specifically allowed 'punning' among union member structs with common initial sequences must be done via an instance of the parent union - rather than being based on the type of the structs (e.g. pointers to them passed to some function). This wording seems to rule out any other interpretation, a la N685. C would do well to adopt this, I'd say. Hey, speaking of which, see below!

The upshot is that - as nicely demonstrated by @ecatmur and in the GCC tickets - this leaves such union member structs by definition in C++, and practically in C, subject to the same strict aliasing rules as any other 2 officially unrelated pointers. The explicit guarantee of being able to read the common initial sequence of inactive union member structs is now more clearly defined, not including vague and unimaginably tedious-to-enforce "visibility" as attempted by N685 for C. By this definition, the main compilers have been behaving as intended for C++. As for C?

Possible reversal of this line in C / clarification in C++

It's also very worth noting that C committee member Martin Sebor is looking to get this fixed in that fine language, too:

Martin Sebor 2015-04-27 14:57:16 UTC If one of you can explain the problem with it I'm willing to write up a paper and submit it to WG14 and request to have the standard changed.

Martin Sebor 2015-05-13 16:02:41 UTC I had a chance to discuss this issue with Clark Nelson last week. Clark has worked on improving the aliasing parts of the C specification in the past, for example in N1520 (http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1520.htm). He agreed that like the issues pointed out in N1520, this is also an outstanding problem that would be worth for WG14 to revisit and fix."

Potatoswatter inspiringly concludes:

The C and C++ committees (via Martin and Clark) will try to find a consensus and hammer out wording so the standard can finally say what it means.

We can only hope!

Again, all further thoughts are welcome.

Is there a problem with common initial sequence with this union of std::array's?

Standard says:

[array.overview]

... An array is a contiguous container. ...

An array is an aggregate that can be list-initialized with up to N elements whose types are convertible to T.

array<T, N> is a structural type if T is a structural type.

The standard doesn't explicitly say what members std::array has. As such, we technically cannot assume that it has a common initial sequence with any type.

From the shown requirements placed on std::array we might reasonably assume that it has a member of type T[N]. Let's explore whether there is a common initial sequence if this assumption is correct.

[class.mem.general]

The common initial sequence of two standard-layout struct ([class.prop]) 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, ...

[basic.types.general]

Two types cv1 T1 and cv2 T2 are layout-compatible types if T1 and T2 are the same type, layout-compatible enumerations, or layout-compatible standard-layout class types.

std::uint8_t[32] and std::uint32_t[8] are not the same type (ignoring cv qualifiers), nor are they enumerations nor classes. Therefore they are not layout-compatible types, and therefore they cannot be part of the same common initial sequence.

Conclusion: No, there is no common initial sequence whether we can safely assume the member of std::array or not.



I write .words and read .bytes

The behaviour of the program is undefined.

Given that you want to read it as an array of (unsigned) char, it would be safe to reinterpret instead of union punning:

static constexpr std::size_t size = 32;
using word = std::uint32_t;
std::array<word, size / sizeof(word)> words {
1, 2, 3, 4,
};
std::uint8_t* bytes = reinterpret_cast<std::uint8_t*>(words.data());

And, if you want a range:

std::span<std::uint8_t, size> bytes_range {
bytes, bytes + size,
};

Initializing union of two structs with common initial sequnce

Regarding aliasing:

The common initial sequence is only concerned with aliasing of the two struct types. That's not a problem here and your two structs are even compatible types and therefore pointers to them may alias without using any tricks. Dissecting C11 6.2.7:

6.2.7 Compatible type and composite type

Two types have compatible type if their types are the same. /--/ Moreover, two structure, union, or enumerated types declared in separate translation units are compatible if their tags and members satisfy the following requirements:

If one is declared with a tag, the
other shall be declared with the same tag.

Neither struct is declared with a tag here.

If both are completed anywhere within their
respective translation units, then the following additional requirements apply:

They are both completed (defined).

there shall
be a one-to-one correspondence between their members such that each pair of
corresponding members are declared with compatible types;

This holds true for these structs.

if one member of the pair is
declared with an alignment specifier, the other is declared with an equivalent alignment
specifier; and if one member of the pair is declared with a name, the other is declared
with the same name.

Alignment specifiers do not apply.

For two structures, corresponding members shall be declared in the
same order.

This holds true.

The conclusion is that your both structs are of compatible types. Meaning that you don't need any tricks like common initial sequence. The strict aliasing rule simply states (6.5/7):

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:

— a type compatible with the effective type of the object,

This is the case here.

Furthermore, as mentioned in other answers, the effective type of the actual data here is int, since allocated storage yields no effective type and so it becomes the first type used for lvalue access. This too means that the compiler cannot assume that the pointers won't alias.

Furthermore, the strict aliasing rule gives an exception for lvalue access of members of structs and unions:

an aggregate or union type that includes one of the aforementioned types among its
members

And then you have the common initial sequence on top of that. As far as aliasing goes, this is as well-defined as can be.


Regarding type punning:

Your actual concern does not seem to be aliasing, but type punning through unions. This is vaguely guaranteed by C11 6.5.2.3/3:

A postfix expression followed by the . operator and an identifier designates a member of a structure or union object. The value is that of the named member,95) and is an lvalue if the first expression is an lvalue.

That's the normative text and it's badly written - nobody can understand how programs/compilers are supposed to behave based on this. The informative foot note 95) explains it well:

95) 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.

Meaning in your case, you trigger a type conversion from one struct type to another compatible struct type. That's perfectly safe since they are the very same type and issues with alignment or traps do not apply.

Please note that C++ is different here.

Does bitfield count as common initial sequence with a whole int of the same type?

By resolution of CWG 645 (for C++11; not sure whether it is supposed to apply to C++98 as DR) the common initial sequence requires corresponding non-static data members or bit-fields in the two classes (by declaration order) to either be both bit-fields (of the same width) or neither be bit-fields.

The wording for that can still be found in [class.mem.general]/23 in the current draft, including an example stating clearly that a bit-field of the same type as a non-static data member will not be part of the common initial sequence.

Therefore the exceptional rule in [class.mem.general]25 allowing access to inactive members in the common initial sequence of standard-layout class members in a union doesn't apply in your case and reading id.value in auto my_id_value = std::uint32_t{id.value}; has undefined behavior.

Type punning a struct in C and C++ via a union

In C:

struct a and struct b are not compatible types. Even in

typedef struct s1 { int x; } t1, *tp1;
typedef struct s2 { int x; } t2, *tp2;

s1 and s2 are not compatible types. (See example in 6.7.8/p5.) An easy way to identify non-compatible structs is that if two struct types are compatible, then something of one type can be assigned to something of the other type. If you would expect the compiler to complain when you try to do that, then they are not compatible types.

Therefore, struct a * and struct b * are also not compatible types, and so struct a and struct b do not share a common initial subsequence. Your union-punning is instead governed by the same rule for union punning in other cases (6.5.2.3 footnote 95):

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.


In C++, struct a and struct b also do not share a common initial subsequence. [class.mem]/p18 (quoting N4140):

Two standard-layout structs share a common initial sequence if
corresponding members have layout-compatible types and either neither
member is a bit-field or both are bit-fields with the same width for a
sequence of one or more initial members.

[basic.types]/p9:

If two types T1 and T2 are the same type, then T1 and T2 are
layout-compatible types. [ Note: Layout-compatible enumerations are described in 7.2. Layout-compatible standard-layout structs and
standard-layout unions are described in 9.2. —end note ]

struct a * and struct b * are neither structs nor unions nor enumerations; therefore they are only layout-compatible if they are the same type, which they are not.

It is true that ([basic.compound]/p3)

Pointers to cv-qualified and cv-unqualified versions (3.9.3) of
layout-compatible types shall have the same value representation and
alignment requirements (3.11).

But that does not mean those pointer types are layout-compatible types, as that term is defined in the standard.

Type Punning via constexpr union

You might use std::bit_cast (C++20):

struct FlagsType
{
unsigned flag0 : 1;
unsigned flag1 : 1;
unsigned flag2 : 1;
unsigned padding : 32 - 3; // Needed for gcc
};

static_assert(std::is_trivially_constructible_v<FlagsType>);

constexpr FlagsType makeFlagsType(bool flag0, bool flag1, bool flag2)
{
FlagsType res{};

res.flag0 = flag0;
res.flag1 = flag1;
res.flag2 = flag2;
return res;
}

static_assert(std::bit_cast<unsigned>(makeFlagsType(true, false, false)) == 1);

Demo

  • clang doesn't support it (yet) though.
  • gcc requires to add explicitly the padding bits for the constexpr check.


Related Topics



Leave a reply



Submit