A Fundamental Difference in Class Behavior between the Native and Managed Object Model

I have used two primary metaphors in my discussions of bring C++ to the .NET platform. In terms of the different physics of the two object models, I have distinguished them as Kansas and Oz, and claimed with little apparent success that the physics of the two are very different. But the biology of the two object models is also different. In native C++ [hold on to any loose items now – this ride may be bumpy!] ontogeny recapitulates phylogeny.

 

Say what?

 

Let me present the definitions necessary, and then the proof.

 

· Ontogeny: The origin and development of an individual organism from embryo to adult. In our case, the construction of an individual object of a derived class.

· Phylogeny: The evolutionary development and history of a species or higher taxonomic grouping of organisms. In our case, the inheritance hierarchy of the class.

· Ontogeny recapitulates phylogeny: The development of the individual organism repeats the adult stage of its ancestors.

In our case, the development of the individual object repeats the adult stage of each of its ancestor classes. I know, you’re saying, Lippman, is this really necessary? In a strange way, I am going to claim that it is. Bear with me.

Consider the following native C++ class hierarchy:

class nat_foo {};

class nat_bar : public nat_foo {};

class nat_foobar : public nat_bar {};

 

What do we know the order of constructor calls when we create either a nat_bar or nat_foobar object? The invocation of constructors is to invoke the base class constructor prior to the invocation of the derived class constructor. Thus, for a nat_foobar object, the order of constructor execution is: nat_foo, nat_bar, and nat_foobar. How is this achieved? The compiler inserts a call to the base class constructor prior to the execution of the program statements of the derived class constructor. So, for example, if nat_foobar’s constructor looked as follows:

 

      nat_foobar::nat_foobar() { cerr << "inside nat_foobar ctor: "; talk_to_me(); }

 

the revised internal body of the constructor would generally look as follows:

 

            {

// inserted call to the base class constructor

this->nat_bar();

// user-supplied program code within the constructor …

cerr << "inside nat_foobar ctor: ";

this->talk_to_me();

}

 

Now, let’s make things interesting. talk_to_me() is a virtual function. Here is what it looks like for our nat_foobar class:

 

            virtual void talk_to_me() { cerr << "i am talking as a nat_foobar ... \n"; }

 

So, all I’ve done is thread up a class hierarchy in which each constructor announces its invocation, calls its virtual talk_to_me() method, and exits. The talk_to_me() method simply announces to which class it belongs. The three classes of the hierarchy each provides its own talk_to_me() instance. This allows us to trace the invocation order of constructors and to discover which virtual function is called within each constructor invocation.

 

I would claim that when the nat_foo() base class constructor is invoked in the creation of a nat_foobar object, one of the following scenarios must occur:

 

  1. the talk_to_me() instance invoked within the nat_foo() constructor is that of the nat_foobar object being constructed.
  2. the talk_to_me() instance invoked within the nat_foo() constructor is that of the nat_bar object whose construction began but then became suspended in order to invoke the nat_foo() constructor.
  3. the talk_to_me() instance invoked within the nat_foo() constructor is that of the nat_foo() class itself.

Which one is it? Well, if ontogeny recapitulates phylogeny [say that quickly 10 times in polite society], then (3) must be the behavior – that is, in the creation of a derived class object, the object becomes an instance of each of its base class ancestors in turn. The nat_foobar object becomes, in turn, a nat_foo object invoking the nat_foo instance of talk_to_me(), then a nat_bar object invoking the nat_bar instance of talk_to_me(), and finally it meets its destiny as a nat_foobar object invoking the nat_foobar instance. If we doubt this, we can hit the lights and run the demo:

 

creating a native 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 ...

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 ...

 

This was not the original behavior of cfront E, the first release of C++ outside of Bell Laboratories, nor was it the behavior of cfront 1.0, the first product release of C++ by the language product group. So, Stroustrup changed the behavior of the language at some point, and the question is why?

 

One possibility, of course, is that Bjarne read the 1874 paper by the German biologist Ernst Haeckel on ontogeny and found it as compelling to his vision of the C++ language as Charles Darwin found Malthus’ views on population to his theory of speciation. To be honest, I have never asked Bjarne whether this is the case.

 

Alternatively, one could probably independently deduce the reasoning behind the change by considering the following:

 

(a) A polymorphic class object is constructed from the inside out, or rather top-down in the hierarchical class order. In our example, the nat_foo sub-object of the nat_foobar object is first constructed [that is, initialized with state and resources], then the nat_bar sub-object [which contains the nat_foo subobject], and then finally the state and resources of the most derived object instance is constructed [that is said a bit sloppily but that is the nature of a blog …]

(b) A virtual method type-specific to a class is likely to refer to the state and resources of that class rather than those of an ancestral base class; that is the nature of a type-dependent function. [This is clearly the axiom, if you will, that is most vulnerable to argument, since I have no quantitative data as to the state and resources most typically referred to within a class virtual method.]

(c) Within a base class constructor that represents a sub-object of a derived class object, the state and resources of the derived class object itself are unconstructed and the resultant behavior of any reference to them is therefore undefined and outside the control of the programmer.

(d) Therefore, it is therefore highly probable that were the base class constructor to invoke the derived class virtual method, that method will make reference to undefined state members and resources, and thus result in anomalous behavior that will be difficult to localize and correct.

 

And so this is why the resolution of a virtual function within the constructor of a class invokes the instance of that function active for that class and not for the most derived class under construction. The implementation challenges to this behavior are actually quite interesting, but the details of that will be postponed to part II of this entry.

 

The question I want to ask is, if we turn the native class hierarchy to a managed class hierarchy, does the same behavior hold? Does ontogeny recapitulates phylogeny under .NET? Well, if the answer was anything but no, this blog would not be quite so pertinent. Let’s dim the lights and run the reel:

 

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

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

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

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 ...

Oh, wow. Isn’t that interesting. Under .NET, the constructors are invoked in the same evaluation order, but the semantics of virtual function resolution reflects choice (1) of the three choices presented above: it is always the instance that is associated with the class of the object under construction.

 

There are a number of things to notice about this: (a) the behavior is less undefined than under native because the creation of an object on the native heap is accompanied by a zeroing out of the actual memory; however, the actual state values and resources are not as yet constructed, and (b) you know have the unintended consequence of multiple invocations of the same method during the construction of the various sub-objects, and these will vary depending on the type of the object being constructed.

 

This difference of behavior, I would claim, is just about comprehended by the Oz/Kansas metaphor. This is one of the fundamental differences between the two object models and why designing one source base to be conditionally compiled as either native or managed is non-trivial. Of course, this example itself is trivial because the virtual function is directly invoked within the constructor and so is open to analysis during compilation. This gets less trivial when there is a call-chain within the constructor such that the presence of a virtual function is not apparent to the compiler. In that case, one must either generate code robust enough to handle the non-trivial case, or else just ignore it completely.