Accredited Standards Committee X3 Doc No: X3J16/94-0185 WG21/N0572 Information Processing Systems Date: Sept 27, 1994 Page 1 of 16 Operating under the procedures of Project: Programming Language C++ American National Standards Institute Ref Doc: Reply to: Josee Lajoie (josee@vnet.ibm.com) +------------------+ | Object Lifetimes | +------------------+ To determine the lifetime of an object, we need to identify the properties that make a piece of memory an object. 1. Properties of Objects ======================== Here are the properties I was able to identify: . address . shape see paper 94-0182/N0569 on objects' shape . polymorphic behavior (virtual function calls, typeid, dynamic_casts) see Richard Minner's paper (94-0183/N0570) on polymorphic behavior . initial value These properties can be divided in there categories: A. The properties of the object that are known as soon as the storage for the object is allocated. These properties are: . static shape . address B. The properties of the object that are set by the implementation (i.e. over which a user has no control) when the object is defined: . dynamic shape . polymorphic behavior The properties in this category do not apply to all objects. Objects of scalar type, for example, do not have such properties. See papers 94-0182/N0569 (dynamic shape) and 94-0183/N0570 (polymorphic behavior) for more details. C. The initial value of the object as specified by the user. Note that C++ doesn't require that every entity be initialized. Some kinds of entities, like const variables are required to be initialized. For other entities, initialization is optional. Note that I have not included cv-qualifiers in the list of object properties that influence the object's lifetime because I do not believe cv-qualifiers are a runtime property associated with an object (see paper 94-0184/N0572. -------- X3J16/94-00185 - WG21/N0572 --- Lajoie:Object Lifetimes --- Page 2 2. Object Lifetimes =================== Proposal: --------- The lifetime of an object starts when the object has acquired the properties described in category A, B (if applicable) and C (if provided). The lifetime of an object ends when the object looses any one of the properties describe in category A or B. Discussion: ----------- I believe the lifetime of an object starts when the implementation's work, necessary to set up the object, has completed (i.e. the object's memory location is known, the location of the virtual base class members is known and the virtual function table is set) and when the object's initialization has completed if any was specified. For example: int i; Note that 'i' does not have a dynamic shape or a polymorphic behavior and is not initialized. However, its lifetime starts as soon as the object's address and static shape is established. Users do not always give an initial value to objects in their program and none-the-less consider these objects as objects. In the example above, 'i' is considered an object of type 'int'. (See paper 94-0181/N0568 for a more complete description on the restrictions that apply to uninitialized objects). Also note that an object may hold many values through its lifetime. Assigning a new value to an object has no effect on its lifetime. For example: int i = 3; //1 i = 5; Here, only one object 'i' exits (and not two). The object is created by the definition on line //1. This object holds the value 3 for a portion of its lifetime and the value 5 for another portion of its lifetime. 3. Object Terminology ===================== o Objects with explicit initialization (category C). An object in this category is either an object with an explicit initializer, an object of class type with a user-defined default constructor or an object of class type that has a sub-object with a user-defined default constructor. -------- X3J16/94-00185 - WG21/N0572 --- Lajoie:Object Lifetimes --- Page 3 o Objects with implicit non-trivial initialization (category B). This category excludes object with explicit initialization. These objects are objects of class type that either have virtual base classes, virtual functions or sub-objects of class type with implicit non-trivial initialization. o Objects with non-trivial initialization (category B & C). These objects are objects with either explicit initialization or objects with implicit non-trivial initialization. Objects of class type with non-trivial initialization have non-trivial constructors: either a user-defined constructor or an implicitly-defined non-trivial default constructor. o Objects with trivial initialization. Note that objects with trivial initialization can be objects of non-class type (like scalars, for example). I believe that to describe the lifetime of objects accurately, we need to define some similar terminology for the destruction of objects of class type. o Objects with explicit destruction (non-trivial destruction). An object in this category is an object of class type with either a user-defined destructor or an object that has a sub-object with a user-defined destructor. o Objects with trivial destruction. Objects with trivial destruction cannot be objects of non-class type (like scalars, for example). Note: ----- The terminology used here assumes that "construction" and "destruction" only apply to objects of class type. Of course, this could be changed. C++ could say that objects of scalar type have trivial implicitly-generated constructors and destructors. This would make the description below simpler. Should C++ be described this way? Proposal: --------- See Appendix A for the suggested WP changes. 4. Lifetime of Declared Objects and Heap Objects ================================================ With the description of object lifetime provided above, we can now describe the lifetime of declared objects and objects allocated on the heap. o The lifetime of declared objects with trivial initialization starts when the object is defined. o The lifetime of declared objects with either no destruction or with trivial destruction ends when the scope in which the object is declared exits. -------- X3J16/94-00185 - WG21/N0572 --- Lajoie:Object Lifetimes --- Page 4 Example, void g() { struct C { int i; float f; } c; /* ... */ } The lifetime of 'c' starts when it is defined and ends when function g exists. o The lifetime of heap objects with trivial initialization starts when the object is defined (i.e. when the library functions new returns). o The lifetime or heap objects with either no destruction or with trivial destruction ends when the object is deallocated (i.e. the library functions delete is called). Example, void g() { int* pi = new int; /* ... */ delete pi; } The lifetime of the object at which 'pi' points starts when new returns and ends when delete is called. Note that since objects with trivial initialization only require that their address and static shape be set up, these objects can be allocated using the malloc C library function. void g() { int* pi = (int *) malloc(sizeof(int)); /* ... */ free pi; } The lifetime of the object at which 'pi' points starts when malloc returns and ends when free is called. o The lifetime of declared or heap objects with non-trivial initialization starts when the object initialization ends. Example: struct T { /* ... */ T(); }; -------- X3J16/94-00185 - WG21/N0572 --- Lajoie:Object Lifetimes --- Page 5 void f() { T t; /* ... */ } The lifetime of 't' starts when when T's constructor exits. Example: void g() { int* pi = new int (99); /* ... */ } The lifetime of the object at which 'pi' points starts when the object's initialization has completed. Note that the malloc C library routine cannot be used to create an object with non-trivial initialization. For example: struct T { /* ... */ T(); }; void h() { T* pt = (T*)malloc(sizeof(T)); // undefined behavior /* ... */ } Only new can be used to create objects with non-trivial initialization. o The lifetime of declared or heap objects with non-trivial destruction ends when the object destruction starts. Example: struct T { /* ... */ ~T(); }; void f() { T t; /* ... */ } The lifetime of 't' ends when when T's destructor starts at the end of function 'f'. Example: class T { /* ... */ public: ~T(); }; -------- X3J16/94-00185 - WG21/N0572 --- Lajoie:Object Lifetimes --- Page 6 void h() { T* pt = new T; delete pt; /* ... */ } The lifetime of the object at which 'pt' points ends when T's destructor starts while the delete expression is executing. Note that the free C library routine cannot be used to destroy an object with non-trivial destruction. For example: struct T { /* ... */ ~T(); }; void h() { /* ... */ free (pt); // ill-formed } Only delete can be used to deallocate objects with non-trivial destruction. Note that any attempt to eliminate an object with non-trivial destruction without ensuring that the destructor is called results in undefined behavior. For example: struct T { /* ... */ ~T(); }; void f() { T t; T *pt = new (&t) T; } This program results in undefined behavior since the object t is eliminated (another object is created at the memory location where t resides using placement new) without t's destructor being called. However, if the object has trivial destruction, this operation is well-defined. For example: struct T { int i; int j; }; void f() { T t; int *pi = new (&t) int; //1 } -------- X3J16/94-00185 - WG21/N0572 --- Lajoie:Object Lifetimes --- Page 7 Line //1 has well-defined behavior because T has trivial destruction and the expression '&t' points at a piece of storage that is large enough to hold an object of type int. Proposal: --------- See Appendix B for the suggested WP changes. 5. Memory Tricks and Objects with Disrupted Lifetimes ===================================================== The examples in this section are variations of examples provided by Jerry Schwarz in core message 2984 and by Jonathan Shopiro in core message 2993. 5.1 Address of objects allocated with placement new --------------------------------------------------- Example: class B { virtual void f(); virtual ~B(); } ; class D1 : public B { void f(); }; class D2 : public B { void f(); }; void* p = malloc(sizeof(D1)+sizeof(D2)); // gets enough space. B* pb1 = new (p) D1; pb1->~B(); // calls virtual destructor. B* pb2 = new (p) D2; // reuses space that has been destroyed. pb2->f(); // calls D2::f(). Does the code above have well-defined behavior? In particular, does p == pb1 == pb2? Resolution: ----------- If p points at a storage location that is large enough to hold either a D1 or a D2, then the code above has well-defined behavior. In particular, p == pb1 == pb2. The description of new with placement says the following: "18.4.1.5.1 [_lib.placement.op.new_] void* operator new(size_t size, void* ptr); Returns ptr." Which indicates that an implementation is required to allocate objects allocated with placement new in such a way that p == pb1 == pb2. This implies that line: B* pb2 = new (p) D2; -------- X3J16/94-00185 - WG21/N0572 --- Lajoie:Object Lifetimes --- Page 8 can therefore be rewritten as: B* pb2 = new ((void*)pb1) D2; Proposal: --------- No change. I believe the WP covers this properly already. Anybody believes something needs to be added to the WP to describe this resolution? 5.2 What can be done with a pointer to an object that has been destroyed? -------------------------------------------------------------------------- Example: class B { virtual void f(); virtual ~B(); static g(); }; class D : public B { void f(); }; void* p = malloc(2*sizeof(D)); // gets enough space. B* pb = new (p) D; pb->~B(); // calls virtual destructor. pb->f(); //1 pb->g(); //2 Do lines //1 and //2 have undefined behavior? That is, what can be done with a pointer to an object that has been destroyed? Can a member function be called? Can a static member be referred to? Here is another example: class C { void f(); void destroy(); static g(); }; void C::destroy () { this->~C(); f(); //1 g(); //2 } Do lines //1 and //2 have undefined behavior? That is, what can be done with the 'this' pointer in a member function after the object referred to by the 'this' pointer has been destroyed? Can a member function be called? Can a static member be referred to? I believe that line //1 in both examples should have undefined behavior, while line //2 in both examples should have well-defined behavior. -------- X3J16/94-00185 - WG21/N0572 --- Lajoie:Object Lifetimes --- Page 9 Resolution: ----------- A pointer to an object of type T that has been destroyed (i.e. p->~T()) can only be used in limited ways. Using the pointer as a T* is no longer valid. However, the pointer still points at valid memory and using the pointer as a pointer to the memory where the object was located '(void *)p' is well-defined. In particular, such a pointer cannot be used to refer to any non-static (data or (virtual or non-virtual) function) members of the destroyed object (i.e. doing so results in undefined behavior). However, such a pointer can be used to access other objects. For example, the pointer can be used to access static data members or call static member functions. Proposal: --------- See Appendix C for the suggested WP changes. 5.3 'new(this) T' in a member function -------------------------------------- Example: const C& C::operator=( const C& other ) { if ( this != &other ) { this->~C(); new (this) C(other); } return *this; } Is this code valid? That is, is it legal to use placement new in a member function to allocate an object at the memory location where the current object resides? Another example: class B { virtual void f(); void mutate(); virtual ~B(); } ; class D1 : public B { void f(); }; class D2 : public B { void f(); }; void B::mutate() { this->~B(); // calls virtual destructor. new (this) D2; // reuses space that has been destroyed. } -------- X3J16/94-00185 - WG21/N0572 --- Lajoie:Object Lifetimes --- Page 10 void* p = malloc(sizeof(D1) + sizeof(D2)); B* pb = new (p) D1; pb->mutate(); pb->f(); // well defined? Is it legal to use placement new in a member function to allocate an object at the memory location where the current object resides if the new object allocated does not have the same type as the 'this' pointer? Resolution: ----------- As indicated in section 5.2, after an object of type T has been destroyed (i.e. p->~T()), using a pointer to that object as a T* is no longer valid but using the pointer as a pointer to the memory where the object was located '(void *)p' is well-defined. This means that, in these examples, after the destructor has been called, the 'this' pointer can be used as a pointer to the memory and be passed to the new with placement library function. However, if a member function allocates an object at the location where the 'this' pointer resides (using placement new), and if the (dynamic) type of the new object allocated is not the same as the (dynamic) type of the object at which the 'this' pointer used to point, then the program results in undefined behavior. That is, the first example above has well-defined behavior. The second example above has undefined behavior. There are obvious optimizations that must be given up if a member function is allowed to allocate an object of a different type at the 'this' address location. Implementation could no longer assume that the shape of the object, (i.e. pointers to virtual base classes, see paper 94-0182) and polymorphic behavior (pointer to vtbl, see paper 94-0183) are unchangeable within a member function. I believe that if the second example was allowed, the cost of loosing these optimizations is too great. This resolution also guarantees that the call 'pb->f();' following the call to the member function 'mutate' will work because 'pb' points at the same type of object as before the 'mutate' member function was called. With the resolution suggested here, implementations can still assume that a member function does not change the type of the object for which the member function is called! Proposal: --------- See Appendix C for the suggested WP changes. -------- X3J16/94-00185 - WG21/N0572 --- Lajoie:Object Lifetimes --- Page 11 5.4 Object Pointer Aliasing --------------------------- Example: class B { virtual void f() ; virtual ~B() ; } ; class D1 : public B { void f(); }; class D2 : public B { void f(); }; void mutate(B** pb2, void *p) { (*pb2)->~B(); *pb2 = new (p) D2; } void* p = malloc(sizeof(D1) + sizeof(D2)); B* pb1 = new (p) D1; mutate( &pb1, p ); pb1->f(); // well-defined? Resolution: ----------- The code above has well-defined behavior. Even if the call to the destructor and the call to placement new are hidden in a function call, implementations are required to assume that the function mutate modifies 'pb1'. (This is strait C aliasing: the pointer 'pb1' has its address taken, implementations must assume that function calls may modify any variable with its address taken). Implementations are therefore required to generate code that guarantees that 'pb->f();' appropriately calls D2::f. Proposal: --------- No change. I believe the WP covers this properly already. Anybody believes something needs to be added to the WP to describe this resolution? 6. Summary (Phases of Construction) =================================== I stole the following diagram from Richard Minner's paper (94-0183). I thought it was very helpful in describing the phases of construction. I changed the diagram a little bit to reflect the phases of construction covered in the other papers provided in this mailing (94-0181, 94-0182, 94-0184) and provided a brief description of the intervals following the diagram. -------- X3J16/94-00185 - WG21/N0572 --- Lajoie:Object Lifetimes --- Page 12 Time proceeds downward Intervals Points --------------------------------- 0c Memory Acquisition [0] Static Shape Acquisition ========================================================= 1c | | Dynamic Shape Acquisition [1] | | --------------------------------- 2c | Object Base Construction [2] | Construction --------------------------------- 3c | | Polymorphic Behavior Acquired [3] | | --------------------------------- 4c | | Member Initialization [4] | | --------------------------------- 5c | | Constructor Execution [5] | | --------------------------------- 6c Object | Memory becomes write-protected [6] Duration | (if applicable) | ================================================= 7c | Object lifetime | ================================================= 7d | | Memory looses write-protection | | (if applicable) | Object --------------------------------- 6d | Destruction Destructor Execution [6] | | --------------------------------- 5d | | Member Destruction | | --------------------------------- 4d | | Polymorphic Behavior Loss | | --------------------------------- 3d | | Base Destruction | | --------------------------------- 2d | | Dynamic Shape Loss ========================================================= 1d Static Shape Loss Memory Release --------------------------------- 0d This diagram applies to the construction of all objects, whether they are complete objects, base class sub-objects or member sub-objects. However, certain intervals in this diagram do not apply objects of non-class type, to objects of class type without virtual base classes or without virtual functions, to base class sub-objects or member sub-objects. The restrictions are as follows: [0] Memory Acquisition and Memory Release only apply when allocating the complete object. Memory acquisition and release for base class and member sub-objects is done when memory for the complete object is acquired and released. -------- X3J16/94-00185 - WG21/N0572 --- Lajoie:Object Lifetimes --- Page 13 The static shape of an object describes the storage layout properties of the object (see 94-0182/N0569) and is acquired when storage for the complete object is allocated and lost when storage for the object is released. [1] Only applies to objects of class type with virtual base classes. It is not quite certain when an object acquires its dynamic shape. Resolutions in paper 94-0182/N0569 will determine how often this interval takes place (only for the complete object, for the complete object and for member sub-objects, or for all sub-objects). [2] Only applies to objects of class type that have base classes with a non-trivial constructor. Construction implies recursive entry through the interval sequence 1,2 4 and 5, for all base class sub-objects. [3] This interval applies to complete objects and to member sub-objects of class type that have virtual functions. The exact moment at which it happens is still under debate. See paper 94-0183/N0570. [4] Only applies to objects of class type that have members with a non-trivial constructor. Construction implies recursive entry through the interval sequence 1 to 5 for all member sub-objects. [5] Only applies to objects of class type that have a non-trivial constructor. The Constructor/Destructor Execution intervals cover exactly the ctor/dtor function bodies: from opening to closing brace. They do not include member or base construction/destruction. [6] If the object was declared 'const', the memory in which the object resides may become write-protected when the object construction is complete and stay write-protected until the object destruction starts. [1d-0d] After an object of type T has been destroyed (i.e. p->~T()), using a pointer to that object as a pointer to T* is no longer valid but using the pointer as a pointer to the memory where the object was located '(void *)p' is well-defined. See section 5 in this paper. Appendix A - Suggested changes to the WP according to resolutions in section 3 ----------------------------------------------------------------- The following text should be added to sub-clause 8.5 [dcl.init]: Objects explicitly initialized (with an explicit initializer) and objects of class type with a non-trivial constructor (12.1) have non-trivial initialization. Other objects have trivial initialization. The WP already contains some of the necessary text in clause 12 describing trivial and non-trivial constructors, trivial and non-trivial -------- X3J16/94-00185 - WG21/N0572 --- Lajoie:Object Lifetimes --- Page 14 initialization for class types. Sub-clause 12.1 [class.ctor] says: 4 An implicitly-declared default constructor is non-trivial if and only if either the class has direct virtual bases or virtual functions or if the class has direct bases or members of a class (or array thereof) with user-defined constructors or with non-trivial implicitly-declared default constructors. Add: User-defined constructors and non-trivial implicitly-declared default constructors are non-trivial constructors. Sub-clause 12.6 [class.init]: 1 A class having a user-defined constructor or having a non-trivial implicitly-declared default constructor is said to require non-trivial initialization. Change to: 1 A class having a non-trivial constructor (12.1) is said to require non-trivial initialization. Add: A program cannot create an object of type T, where T is a class type that requires non-trivial initialization, without calling T's constructor explicitly or implicitly. Any attempt to do otherwise results in undefined behavior. For example: struct S { /* ... */ S(); }; void h() { S* pt = (S*)malloc(sizeof(S)); // ill-formed } The WP sub-clause 12.4 [class.dtor] needs to be updated to describe trivial and non-trivial destructors, trivial and non-trivial destruction for class types. Sub-clause 12.4 [class.dtor]: 2 If a base or a member of a class has a destructor and no destructor is declared for the class itself a default destructor is generated. This generated destructor calls the destructors for bases and members of its class. Generated destructors are public. Change to: 2 If no destructor has been declared for class X, a destructor is implicitly defined. An implicitly-defined destructor is non-trivial if and only if the class has direct bases or members of a class (or array thereof) with user-defined destructors or with non-trivial implicitly-defined destructors. User-defined destructors and non-trivial implicitly-defined destructors are non-trivial destructors. A class having a non-trivial destructor is said to require non-trivial destruction. A program cannot eliminate an object of type T, where T is a class type that requires non-trivial destruction, without calling T's destructor explicitly or implicitly. Any attempt to do otherwise results in undefined behavior. -------- X3J16/94-00185 - WG21/N0572 --- Lajoie:Object Lifetimes --- Page 15 Appendix B - Suggested changes to the WP according to resolutions in section 4 ----------------------------------------------------------------- The following text from sub-clause 1.5 [intro.memory] should be changed: 2 The lifetime of an object begins after any required initialization has completed. For objects with destructors, it ends when destruction starts. Change to: 2 The lifetime of an object is determined by the object storage duration (3.6). The following text should be added to sub-clause 3.6 [basic.stc]: 1 A program has undefined behavior if it creates an object with non-trivial initialization and omits the initialization (12.1); if it eliminates an object with non-trivial destruction without calling the destructor (12.4); if it destroys an object with non-trivial destruction twice (12.4). The following text should be added to sub-clause 3.6.1 [basic.stc.static]: (maybe this should go in 3.5.2 [basic.start.init] and 3.5.3 [basic.start.term] ??) 1 The lifetime of an object with static storage duration that has trivial initialization (8.5) starts when the object is defined; if the object has non-trivial initialization, its lifetime starts when its initialization has completed. The lifetime of an object with static storage duration that has either no destruction or that has trivial destruction (12.4) ends at the end of the program; if the object has non-trivial destruction its lifetime ends when its destruction starts. The following text should be added to sub-clause 3.6.2 [basic.stc.auto]: (maybe this should go in 6.7 [stmt.dcl] ??) 2 The lifetime of objects with automatic storage duration that have trivial initialization (8.5) starts when the object is defined; if these objects have non-trivial initialization, their lifetime starts when their initialization has completed. The lifetime of objects with static storage duration that have either no destruction or that have trivial destruction (12.4) ends at when the scope in which the object is declared exits; if these objects have non-trivial destruction their lifetime ends when their destruction starts. The following text should be added to sub-clause 3.6.3 [basic.stc.dynamic]: 1 The lifetime of objects with dynamic storage duration that have trivial initialization (8.5) starts when the object is defined when the appropriate allocation function returns; if these objects have non-trivial initialization, their lifetime starts when their initialization has completed. The lifetime of objects with dynamic storage duration that either have no destruction or that have trivial destruction (12.4) ends when the object is deallocated; if -------- X3J16/94-00185 - WG21/N0572 --- Lajoie:Object Lifetimes --- Page 16 these objects have non-trivial destruction their lifetime ends when their destruction starts. Appendix C - Suggested changes to the WP according to resolutions in section 5 ----------------------------------------------------------------- The following text should be added to sub-clause 12.4 [class.dtor]: 8 A pointer to an object of type T that has been destroyed (p->~X()) can only be used in limited ways. Using the pointer as an X* is no longer valid. However, the pointer still points at valid memory and using the pointer as a pointer to the memory where the object was located '(void *)p' is well-defined. In particular, such a pointer cannot be used to refer to any non-static (data or (virtual or non-virtual) function) members of the destroyed object (i.e. doing so results in undefined behavior). However, such a pointer can be used to access other objects. For example, the pointer can be used to access static data members or call static member functions. The following text should be added to sub-clause 12.5 [class.free]: 12 If a member function allocates an object at the location where the 'this' pointer resides (using placement new), and if the (dynamic) type of the new object allocated is not the same as the (dynamic) type of the object at which the 'this' pointer used to point, then the program results in undefined behavior.