다음을 통해 공유


Making a Virtual Table Context-Sensitive

In part 1 of this discussion [the January 28th Blog entry], I pointed out a different behavior between the C++ and CLR Object Models with regard the identity of a derived polymorphic object during the construction of its base class sub-objects. Under the CLR object model, the embryonic derived object is always treated as an instance of its class within each base class constructor; therefore, any virtual function invocations within the call chain initiated within the base class constructor resolves to the derived class instance. Under ISO C++, the embryonic derived object is in turn treated as an instance of its base class sub-object currently under construction; therefore, each virtual function invocation within the call chain initiated within the base class constructor resolves to the instance active for an object of that base class and not the instance active for the derived class. I illustrated this as follows:

 

creating a native foobar : bar : foo {} instance ...

inside nat_foo ctor: i am talking as a nat_foo ...

inside nat_bar ctor: i am talking as a nat_bar ...

inside nat_foobar ctor: i am talking as a nat_foobar ...

 

creating a managed foobar : bar : foo {} instance ...

inside foo ctor: i am talking as a managed foobar ...

inside bar ctor: i am talking as a managed foobar ...

inside foobar ctor: i am talking as a managed foobar ...

 

and pointed out that were the derived instance of the virtual function to access a state member or resource of the derived class object, the results would be undefined since the derived class object’s state members and resources have yet to be constructed.

 

There is run-time overhead in the maintenance of the C++ Object Model semantics, and I thought I would explore that in this follow-up entry. There are two problem cases to solve: (1) the direct invocation of a virtual function within the constructor [easy], and (2) the indirect invocation of a virtual function through an arbitrary call chain [hard]. The first case is easy because we can always resolve it statically: that is, within the nat_foo constructor, always resolve an invocation of the virtual talk_to_me() instance to that defined within the nat_foo class. So it is the second case only that warrants further discussion.

 

Consider the following class hierarchy:

 

            class Base {

public:

      virtual void bar();

void foo() { bar(); }

Base() { foo(); } // 1

};

class Derived : public Base {

      public:

            virtual void bar(); // overrides Base::foo()

           

            Derived(){ foo(); } // 2

            // …

      };

      int main()

{

Base *pbase = new Derived; // invokes (1) and then (2)

pbase->foo(); // 3

}

 

The behavior that the ISO C++ compiler has to support is context sensitive. The invocation of foo() at all times is resolved to Base::foo(), which is a non-virtual inline method invoking the virtual bar() defined in both Base and Derived. The actual of bar() within foo() is determined by the implicit this pointer, so you can imagine the call being expanded to look something like this:

 

this->bar();

where the actual type of the this pointer at each call point determines which instance of bar() is invoked. Base::foo() is invoked three times, marked in the program by // 1, // 2, and // 3, and result in the following instances of bar():

 

  1. The Base constructor is invoked to initialize the sub-object of the Derived class object allocated in the first line of main(). It invokes foo() which in turn resolves to the Base::bar() virtual method being invoked.

 

  1. The Derived constructor is invoked subsequent to the completion of the Base constructor, and completes the initialization of the Derived class object allocated in the first line of main(). It invokes foo() as well which in turn resolves to the Derived::bar() virtual method.

 

  1. The invocation of foo() directly within the second line of main() through the polymorphic pbase object results in the invocation of Derived::bar().

 

So, the implementation difficulty is that within foo() we only want to suppress the virtual mechanism if foo() is invoked during the execution of a constructor. In this special case, we always invoke the instance of bar() associated with the class of the executing constructor and not for the class of the type under construction. [That is, when foo() is invoked within the Base constructor, the Base::bar instance should always be invoked. Similarly, when foo() is invoked within the Derived constructor, the Derived::bar instance should always be invoked.] Otherwise, the regular virtual function call resolution should kick in. [That is, within main(), the instance of bar() being invoked is determined by the type of object addressed by pbase.]

 

So, how might we do that? One possible solution is to introduce a global entity which either points to the class whose constructor is being executed or is set to 0. foo() is then rewritten by the compiler to test the entity and, if non-zero, to invoke the instance associated with the pointed to class. If the entity is 0, the call would then go through the normal virtual mechanism. This would have a rather significant impact on the program and, in fact, is not a preferred solution.

 

So, what can we do? Well, rather than trying to sensitive each and every call point, why not try to sensitive the virtual mechanism itself to be aware of whether or not the call is originating from within a constructor. How might we do that?

Consider for a moment what actually determines the virtual function set active for a class: the virtual table. How is that virtual table accessed, and therefore the active set of virtual functions determined? By the address to which the virtual table pointer [vptr] within the polymorphic object is set. To control the set of active functions for a class, therefore, the compilation system need simply control the initialization and setting of the vptr. (It is the compiler’s responsibility to set the vptr, of course, not something the programmer need or should worry about.)

The solution is to set the vptr within each class constructor after invocation of its base class constructors, but prior to the execution of user provided code or the expansion of data members initialized within the member initialization list. In this way, within each base class constructor throughout the class hierarchy, the derived class object under construction literally becomes an object of the class for the duration of the base class constructor. This is how the Derived class object becomes a Base class object within the Base class constructor and back to a Derived class object within its own constructor. Within each base class constructor, it is indistinguishable from a complete object of the constructor’s class. For derived class objects, as I suggested in the part 1, ontogeny recapitulates phylogeny. 

The general algorithm of constructor execution is as follows:

1. Within the derived class constructor, all virtual base class then immediate base class constructors are invoked.

2. That done, the object’s vptr(s) are initialized to address the associated virtual table(s).

3. The member initialization list, if present, is expanded within the body of the constructor. This must be done after the vptr is set in case a virtual member function is called.

4. The explicit user-supplied code is executed.

The vptr always has to be set within the constructor of the actual class of the object being initialized, so that in itself is a necessary overhead of the virtual mechanism. This is why bitwise copy semantics or the use of memset/memcpy is not permissible for classes in which a vptr is present. The additional runtime overhead in support of this type evolution within sub-object construction is the resetting of the vptr within each base class constructor as well. Again, the purpose of this is to reassign the active virtual table for virtual method resolution within the execution of each base class constructor. This is the implementation variation within the two object models under discussion.

Comments