В новом стандарте это еще более зарегламентированно, правда, разбросано по разным местам.
C++17 писал(а):
6.8(1). The lifetime of an object or reference is a runtime property of the object or reference. An object is said to have non-vacuous initialization if it is of a class or aggregate type and it or one of its subobjects is initialized by a
constructor other than a trivial default constructor. [ Note: Initialization by a trivial copy/move constructor is non-vacuous initialization. — end note ] The lifetime of an object of type T begins when:
(1.1) — storage with the proper alignment and size for type T is obtained, and
(1.2) — if the object has non-vacuous initialization, its initialization is complete, except that if the object is a union member or subobject thereof, its lifetime only begins if that union member is the initialized member in the union (11.6.1, 15.6.2), or as described in 12.3. The lifetime of an object o of type T ends when:
(1.3) — if T is a class type with a non-trivial destructor (15.4), the destructor call starts, or
(1.4) — the storage which the object occupies is released, or is reused by an object that is not nested within o (4.5).
6.8(4). The properties ascribed to objects and references throughout this International Standard apply for a given object or reference only during its lifetime. [ Note: In particular, before the lifetime of an object starts and after its lifetime ends there are significant restrictions on the use of the object, as described below, in 15.6.2 and in 15.7. Also, the behavior of an object under construction and destruction might not be the same as the behavior of an object whose lifetime has started and not ended. 15.6.2 and 15.7 describe the behavior of objects during the construction and destruction phases. — end note ]
6.8(7). Similarly, before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any glvalue that refers to the original object may be used but only in limited ways. For an object under construction or destruction, see 15.7. Otherwise, such a glvalue refers to allocated storage (6.7.4.2), and using the properties of the glvalue that do not depend on its value is well-defined. The program has undefined behavior if:
(7.1) — the glvalue is used to access the object, or
(7.2) — the glvalue is used to call a non-static member function of the object, or
(7.3) — the glvalue is bound to a reference to a virtual base class (11.6.3), or
(7.4) — the glvalue is used as the operand of a dynamic_cast (8.2.7) or as the operand of typeid.
6.10(1.1) A glvalue is an expression whose evaluation determines the identity of an object, bit-field, or function.
12(3). A union is a class defined with the class-key union; it holds at most one data member at a time (12.3). [...]
12.3(1). In a union, a non-static data member is active if its name refers to an object whose lifetime has begun and has not ended (6.8). At most one of the non-static data members of an object of union type can be active at any time, that is, the value of at most one of the non-static data members can be stored in a union at any time. [ Note: One special guarantee is made in order to simplify the use of unions: If a standard-layout union contains several standard-layout structs that share a common initial sequence (12.2), and if a non-static data member of an object of this standard-layout union type is active and is one of the standard-layout structs, it is permitted to inspect the common initial sequence of any of the standard-layout struct members; see 12.2. — end note ]
12.3(5). When the left operand of an assignment operator involves a member access expression (8.2.5) that nominates a union member, it may begin the lifetime of that union member, as described below. For an expression E, define the set S(E) of subexpressions of E as follows:
(5.1) — If E is of the form A.B, S(E) contains the elements of S(A), and also contains A.B if B names a union member of a non-class, non-array type, or of a class type with a trivial default constructor that is not deleted, or an array of such types.
(5.2) — If E is of the form A[B] and is interpreted as a built-in array subscripting operator, S(E) is S(A) if A is of array type, S(B) if B is of array type, and empty otherwise.
(5.3) — Otherwise, S(E) is empty.
In an assignment expression of the form E1 = E2 that uses either the built-in assignment operator (8.18) or a trivial assignment operator (15.8), for each element X of S(E1), if modification of X would have undefined behavior under 6.8, an object of the type of X is implicitly created in the nominated storage; no initialization is performed and the beginning of its lifetime is sequenced after the value computation of the left and right operands and before the assignment. [ Note: This ends the lifetime of the previously-active member of the union, if any (6.8). — end note ]
Рассмотрим Ваш код:
#define CHARS_PER_LONG 4
union Long_And_Char {
private:
long long_value;
char char_values[CHARS_PER_LONG];
public:
Long_And_Char() {}
Long_And_Char(long v) { long_value = v; }
operator long & () { return(long_value); }
char & operator [] (size_t idx) { return(char_values[idx]); }
};
Здесь пока неопределенного поведения нет. Но если мы начнем использовать этот union для целей нашей задачи, т.е. напишем, например,
Long_And_Char x(12);
char c = x[0];
то во второй строке получим undefined behavior. Следите за руками: в первой строке вызывается конструктор Long_And_Char, и в соответствии с 6.8(1.2) и 12.3(5) по завершении этого конструктора начинается lifetime объектов
x и
x.long_value, но не
x.char_value. Теперь
x.long_value является active member of the object
x.
Во второй строчке мы вызываем оператор индексации, и пытаемся получить доступ к объекту
x.char_values[0] через glvalue
char_values[idx], встречающееся в реализации оператора. И получаем undefined behavior, описанный в 6.8(7.1).