This is an unofficial snapshot of the ISO/IEC JTC1 SC22 WG21 Core Issues List revision 113d. See http://www.open-std.org/jtc1/sc22/wg21/ for the official list.

2024-03-20


1116. Aliasing of union members

Section: 6.7.3  [basic.life]     Status: CD4     Submitter: US     Date: 2010-08-02

[Adopted at the June, 2016 meeting as document P0137R1.]

N3092 comment US 27

Related to issue 1027, consider:

    int f() {
      union U { double d; } u1, u2;
      (int&)u1.d = 1;
      u2 = u1;
      return (int&)u2.d;
    }

Does this involve undefined behavior? 6.7.3 [basic.life] paragraph 4 seems to say that it's OK to clobber u1 with an int object. Then union assignment copies the object representation, possibly creating an int object in u2 and making the return statement well-defined. If this is well-defined, compilers are significantly limited in the assumptions they can make about type aliasing. On the other hand, the variant where U has an array of unsigned char member must be well-defined in order to support std::aligned_storage.

Suggested resolution: Clarify that this case is undefined, but that adding an array of unsigned char to union U would make it well-defined — if a storage location is allocated with a particular type, it should be undefined to create an object in that storage if it would be undefined to access the stored value of the object through the allocated type.

(See also issues 1027 and 1338.)

Proposed resolution (August, 2010):

  1. Change 6.7.3 [basic.life] paragraph 1 as follows:

  2. ...The lifetime of an object of type T begins when storage with the proper alignment and size for type T is obtained, and either:

    The lifetime of an object of type T ends...

  3. Change 6.7.3 [basic.life] paragraph 4 as follows:

  4. A program may end the lifetime of any object by reusing the storage which the object occupies or by explicitly calling the destructor for an object of a class type with a non-trivial destructor. For an object of a class type with a non-trivial destructor, the program is not required to call the destructor explicitly before the storage which the object occupies is reused or released; however, if there is no explicit call to the destructor or if a delete-expression (7.6.2.9 [expr.delete]) is not used to release the storage, the destructor shall not be implicitly called and any program that depends on the side effects produced by the destructor has undefined behavior. If a program obtains storage for an object of a particular type A (e.g. with a variable definition or new-expression) and later reuses that storage for an object of another type B such that accessing the stored value of the B object through a glvalue of type A would have undefined behavior (7.2.1 [basic.lval]), the behavior is undefined. [Example:

      int i;
      (double&)i = 1.0; // undefined behavior
    
      struct S { unsigned char alignas(double) ar[sizeof (double)]; } s;
      (double&)s = 1.0; // OK, can access stored double through s because it has an unsigned char subobject
    

    end example]

  5. Change 7.2.1 [basic.lval] paragraph 10 as follows:

  6. 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 undefined52:

This resolution also resolves issue 1027.

Additional note (August, 2012):

Concerns have been raised regarding the interaction of this change with facilities like std::aligned_storage and memory pools. Care must be taken to achieve the proper balance between supporting type-based optimization techniques and allowing practical storage management.

Additional note (January, 2013):

Several questions have been raised about the wording above . In particular:

  1. Since aggregates and unions cannot have base classes, why are base classes mentioned?

  2. Since unions can now have special member functions, is it still valid to assume that they alias all their member types?

  3. Shouldn't standard-layout classes also be considered and not just aggregates?

Additional note, February, 2014:

According to 6.7.2 [intro.object] paragraph 1, an object (i.e., a “region of storage”) is created by one of only three means:

An object is created by a definition (6.2 [basic.def]), by a new-expression (7.6.2.8 [expr.new]) or by the implementation (6.7.7 [class.temporary]) when needed. The properties of an object are determined when the object is created.

This does not allow for obtaining the storage in other ways, such as via malloc, in determining the lifetime of an object with vacuous initialization (6.7.3 [basic.life] paragraph 1).

In addition, 6.7.3 [basic.life] paragraph 1 does not require the storage obtained for an object of type T to be accessed via an lvalue of type T in order to be considered an object of that type. The treatment of “effective type” by C may be helpful here.

Additional note, May, 2015:

We never say what the active member of a union is, how it can be changed, and so on. The Standard doesn't make clear whether the following is valid:

    union U { int a; short b; } u = { 0 };
    int x = u.a; // presumably this is OK, but we never say that a is the active member
    u.b = 0;     // not clear whether this is valid 

The closest we come to talking about this is the non-normative example in 11.5 [class.union] paragraph 4, which suggests that a placement new is needed.

It's also not clear whether a has two subobjects or only one (corresponding to the active member).