TITLE: Assignment of dynamic type (long explaination) [ This is a detailed discussion of when exactly the dynamic type of an object is assigned or changed. If you are not into details, your eyes may glaze over on this one. -adc ] JS = maxtal@Physics.usyd.edu.au (John Max Skaller), 17 Feb 95 JP = Jonathan de Boyne Pollard FH = fjh@munta.cs.mu.OZ.AU (Fergus Henderson) JP: When is the dynamic type of an instance assigned and removed in its constructor and destructor ? Most of the documentation available to me right at this moment is pretty vague on the subject. JS: This question has been the subject of extensive and intensive research by the core group of the committee. The answer is not as simple as you think, it is not just a matter of "the" dynamic type. The dynamic type _changes_ during construction, and there is more than one entity during construction that has a dynamic type. Separately, being able to address components is an issue. When can you address a member of a virtual base of a member? The dynamic type is established after the bases have been fully initialised but before the members. (As far as I can remember) The original resolution for addressability was "everything is addressable immediately after evaluating the arguments of the constructor of the most derived class" but that was not put in the Working Paper because it precludes constructors setting up virtual base pointers. JP: It seems that this would be the sort of thing that would be implementation defined, or possibly not be required to be defined at all. JS: No, the committee has a much tighter requirement. Do you want all virtual function calls in constructors to fail? [...] JP: It seems that this would be the sort of thing that would be implementation defined, or possibly not be required to be defined at all. We would be guaranteed that the dynamic type of an instance would be correct from after the completion of the constructor up until the start of the destructor. But nothing that I can find will actually confirm this. FH: The latest working paper does guarantee that. JP: Some experimentation leads me to believe that most implementations assign dynamic type at the start of the constructor body after any member or base class initialisers, and remove it at the end of the destructor body before any member or base class destructors. FH: According to the latest working paper, the dynamic type must be assigned before the member initializers are executed. Conversely, it should not be removed until after the member destructors are executed. So it sounds like those implementations don't quite conform. However, implementations that assign dynamic type after any base class initialisers but before any member initializers, and remove it after any member destructors but before any base class destructors, would be conforming. JS: That is not quite correct. The dynamic type has to be assigned _between_ the completion of base initialisation and before the constructor of the first member is called -- no sooner and no later. That means the only "flexibility" is during the evaluation of the arguments to the constructor call of a the first member to be initialised. The reason is obvious -- virtual dispatches invoked dynamically in a base constructor had better NOT dispatch past the type of the class whose constructor body is executing. That is, calling an impure virtual function should always work (if the this pointer of the static type can be calculated). JP: However, this is not universally the case. At least two of the C++ compilers in my possession (both of which are DirectToSOM C++ compilers) assign the dynamic type to an instance before any member or base class initialisers are invoked. Are there weasel words to accommodate both ? Or are there more rigid guarantees ? FH: There are weasel words to accomodate both (with the correction to the first group that I mentioned above). We spent quite a lot of time finding the right weasel words ;-) The appropriate parts of the latest working paper follow. 8 Member functions (including virtual member functions, _class.virtual_) can be called for an object under construction. Similarly, an object under construction can be the operand of the typeid operator (_expr.typeid_) or of a dynamic_cast (_expr.dynamic.cast_). However, if these operations are performed in a ctor-intializer (or in a func­ tion called directly or indirectly from a ctor-intializer) before all the mem-initializers for base classes have completed, the result of the operation is undefined. For example: class A { public: A(int); }; class B : public A { int j; public: int f(); B() : A(f()), // undefined: calls member function // but base A not yet initialized j(f()) { } // well-defined: bases are all initialized }; class C { public: C(int); }; class D : public B, C { int i; public: D() : C(f()), // undefined: calls member function // but base C not yet initialized i(f()) {} // well-defined: bases are all initialized }; 9 _class.cdtor_ describes the result of virtual function calls, typeid and dynamic_casts during construction for the well-defined cases; that is, describes the polymorphic behavior of an object under construc­ tion. 12.7 Construction and destruction [class.cdtor] 1 For an object of non-POD class type (_class_), before the constructor begins execution and after the destructor finishes execution, refer­ ring to any nonstatic member or base class of the object results in undefined behavior. For example, struct X { int i; }; struct Y : X { }; struct A { int a; }; struct B : public A { int j; Y y; }; extern B bobj; B* pb = &bobj; // ok int* p1 = &bobj.a; // undefined, refers to base class member int* p2 = &bobj.y.i; // undefined, refers to member's member A* pa = &bobj; // undefined, upcast to a base class type B bobj; // definition of bobj extern X xobj; int* p3 = &xobj.i; // Ok, X is a POD class X xobj; 2 Example struct W { int j; }; struct X : public virtual W { }; struct Y { int *p; X x; Y() : p(&x.j) // undefined, x is not yet constructed { } }; 3 To explicitly or implicitly convert a pointer to an object of class X to a pointer to a direct or indirect base class B, the construction of X and the construction of all of its direct or indirect bases that directly or indirectly derive from B and which are also direct or indirect base classes of X6) shall have started and the destruction of these classes shall not have completed, otherwise the computation results in undefined behavior. To form a pointer to a direct non­ static member of an object X given a pointer to X, the construction of X shall have started and the destruction of X shall not have com­ pleted, otherwise the computation results in undefined behavior. For example, struct A { }; struct B : virtual A { }; struct C : B { }; struct D : virtual A { D(A*); }; struct X { X(A*); }; struct E : C, D, X { E() : D(this), // undefined: upcast from E* to A* // might use path E* -> D* -> A* // but D is not constructed // D((C*)this), // defined: // E* -> C* defined because E() has started // and C* -> A* defined because // C fully constructed X(this) // defined: upon construction of X, // C/B/D/A sublattice is fully constructed { } }; 4 Member functions, including virtual functions (_class.virtual_), can be called during construction or destruction (_class.base.init_). When a virtual function is called directly or indirectly from a con­ structor (including from its ctor-initializer) or from a destructor, the function called is the one defined in the constructor or destruc­ tor's own class or in one of its bases, but not a function overriding it in a class derived from the constructor or destructor's class or overriding it in one of the other base classes of the complete object (_intro.object_). If the virtual function call uses an explicit class member access (_expr.ref_) and the object-expression's type is neither the constructor or destructor's own class or one of its bases, the _________________________ 6) If X is itself a base class, not all classes derived from B are necessarily base classes of X. result of the call is undefined. For example, class V { public: virtual void f(); virtual void g(); }; class A : public virtual V { public: virtual void f(); }; class B : public virtual V { public: virtual void g(); B(V*, A*); }; class D : public A, B { public: virtual void f(); virtual void g(); D() : B((A*)this, this) { } }; B::B(V* v, A* a) { f(); // calls V::f, not A::f g(); // calls B::g, not D::g v->g(); // v is base of B, the call is well-defined, calls B: :g a->f(); // undefined behavior, a's type not a base of B } 5 The typeid operator (_expr.typeid_) can be used during construction or destruction (_class.base.init_). When typeid is used in a constructor (including in its ctor-initializer) or in a destructor, or used in a function called (directly or indirectly) from a constructor or destructor, if the operand of typeid refers to the object under con­ struction or destruction, typeid yields the type_info representing the constructor or destructor's class. If the operand of typeid refers to the object under construction or destruction and the static type of the operand is neither the constructor or destructor's class nor one of its bases, the result of typeid is undefined. 6 Dynamic_casts (_expr.dynamic.cast_) can be used during construction or destruction (_class.base.init_). When a dynamic_cast is used in a con­ structor (including in its ctor-initializer) or in a destructor, or used in a function called (directly or indirectly) from a constructor or destructor, if the operand of the dynamic_cast refers to the object under construction or destruction, this object is considered to be a complete object that has the type of the constructor or destructor's class. If the operand of the dynamic_cast refers to the object under construction or destruction and the static type of the operand is not a pointer to or object of the constructor or destructor's own class or one of its bases, the dynamic_cast results in undefined behavior. 7 Example class V { public: virtual void f(); }; class A : public virtual V { }; class B : public virtual V { public: B(V*, A*); }; class D : public A, B { public: D() : B((A*)this, this) { } }; B::B(V* v, A* a) { typeid(this); // type_info for B typeid(*v); // well-defined: *v has type V, a base of B // yields type_info for B typeid(*a); // undefined behavior: type A not a base of B dynamic_cast(v); // well-defined: v of type V*, V base of B // results in B* dynamic_cast(a); // undefined behavior, // a has type A*, A not a base of B }