Author: Professor Alex Aiken, Computer Science Division, University of California, Berkeley. Notes on the Implementation of C++ ---------------------------------- I. Overview This lecture covers the implementation of objects and inheritance in OO languages, with particular attention paid to C++. The features covered are: 1. Single inheritance 2. Dynamic dispatch ("virtual functions" in C++) 3. Multiple inheritance 4. Parameterized classes ("templates" in C++) II. Object Layout For this lecture, the syntax used is approximately C++. The following is a program with one simple C++ class definition: #include class A { public: int a; virtual int geta() { return a; } virtual void seta(int b) { a = b; } }; main() { A o1; o1.seta(4); printf("%d\n",o1.geta()); } This program defines a class "A" with an integer "a" and two functions "geta" and "seta". Objects of the class can be visualized as a record of three components: ------------------------- | a | seta | geta | ------------------------- The "a" slot holds an integer, the seta slot holds a pointer to the code for "seta", and the geta slot holds a pointer to the "geta" function. Note that the C++ compiler knows the type of every expression. For example, in the program above, "o1" is known to be of type "A". The compiler determines, and therefore knows, the layout of objects of type "A". Thus, the expression "o1.geta()" is easily translated into an ordinary C function call by selecting the appropriate component of the record "o1". For the example above, the translation is something like: o1.a ==> o1-> a o1.geta() ==> o1->geta(o1) o1.seta(i) ==> o1->seta(o1,i) Note that the translated function call includes "o1" as an extra argument. Every use of a method in a class X is passed the object of type X used in the method call. This value is the "this" parameter in the method body (in other OO languages, "this" is called "self"). We'll touch on the reason for having self below. Now, the implementation described above is a lie. Objects are not implemented this way. Note that the functions in an object are "immutable"---they are never modified and are common to all objects of the class. Thus, the functions for a class are separated out into a "dispatch" table for the class (called a "vtable" in C++). The true representation for the class A above is: ----------------------- | disptach ptr | a | ----------------------- | ------------------- |-->| seta | geta | ------------------- All objects of the class share the same dispatch table. It is important that the dispatch table be located at a fixed offset from the beginning of the dispatch table, so that one calling convention will work for all classes; in this case, the offset is 0. The translation scheme is now: o1.a ==> o1->a o1.seta(3) ==> (*(o1->vptr[0]))(o1,3) o1.geta() ==> (*(o1->vptr[1]))(o1) Translating from the ugly syntax, the semantics of e.f(...) is to call the function "f" in the vtable of object "e". Thus, a method call adds the cost of a table lookup over a normal function call. III. Single Inheritance An opportunity for code reuse arises whenever one wants to extend an existing class with additional functionality. For example, a "B" that behaves like an "A" except that it has an additional field that can be set/get is: class B : A { public: int b; virtual int getb() {return b;} virtual void setb(int c) {b = c + a;} virtual void seta(int c) {a = c+1;} }; main() { A o1; o1.seta(4); printf("%d\n",o1.geta()); B o2; o2.seta(2); o2.setb(3); printf("%d\n",o2.getb()); } The syntax "class B : A" is C++ for "B inherits from A". A "B" is represented by extending the representation of "A" with new fields: ---------------------------- | dispatch ptr | a | b | ---------------------------- | ----------------------------------------- |-------->| B::seta | A::geta | B::setb | B::getb | ----------------------------------------- Note that the "B" class has provided a new definition of "seta" for "B" objects which overrides the definition inherited from "A". In the dispatch table, the position for "seta" contains a pointer to B's seta instead of A's seta. Now, for an important point. Note that the form of a "B" object is just an extension of an "A" object. That is, by "forgetting" trailing fields of the object and dispatch table, we have an "A" object. Thus, the "B" object can be used wherever an "A" object is permitted. In particular, the method "geta" of class "A" works for "B" objects. In general, if Xn inherits from Xn-1 inherits from ... X1, then the object layout will be -------------------------------------------------------------- | dispatch ptr | X1's data | X2's data | . . . | Xn's data | -------------------------------------------------------------- | ------------------------------------------------------ ----------->| X1's methods | X2's methods | . . . | Xn's methods | ------------------------------------------------------ Every prefix of the object up to Xi's data/methods is a valid Xi object for every i. IV. A Note on Self ("This") In C++ terminology, if B inherits A then B is the "derived class" from A and A is a "base class" of B. As the previous example illustrates, a derived class may override the methods of a base class. This feature exists in every OO language, but gives rise to the following situation. Consider a class A with a method f and method g that calls f: class A { ... virtual ... f(...) {...} virtual ... g(...) {... this.f(...) ...} } Now, the syntax suggests that the use of "f" in "g" binds to the method "f" defined in class A, but this is *not* necessarily the case, because "f" may be overridden by a class derived from A. That is, the even though "this" is regarded as being of class A in the method body, it may be some class derived from A with a different "f" method. This example at least partly motivates the need for "this". If there were no "this" parameter to methods, then derived classes could never override the behavior of a method and have that new behavior respected when calling a method from code in a base class. V. Multiple Inheritance Multiple inheritance is a controversial language feature. It is not new with C++; the idea has been around for a long time. The controversy stems mostly from semantic problems with defining how multiple inheritance should work in all situations. The basic implementation idea is not difficult. Consider a class that inherits from two base classes: class A : B,C { ... }; The layout of the object could look like: -------------------------------------------------------- | dispatch ptr | B's data | C's data | A's data | -------------------------------------------------------- | ------------------------------------------------- ------------>| B's methods | C's methods | A's methods | ------------------------------------------------- This scheme clearly works for calls to A methods, since the compiler knows that this layout is used for A objects. Similarly, the layout works for B methods, because the A layout is just an extension of the B layout. But what about C methods? What is the "this" parameter for a call to a method C::f? This layout is *not* an extension of a C object, because B's data and methods are listed first. The solution is that whenever a C::f method is called, the "this" pointer is adjusted to point to the beginning of the "C" part of the object. This idea has a few ramifications: 1. The "C" part of the layout must begin a valid object, so a dispatch table pointer is needed at the beginning of "C". Since the "this" value is only adjusted to point to the C component when a C method is called, we know that the method being called is defined in class C. Thus, the dispatch table beginning the C component must have a dispatch table with the same entries as any other object of class C. 2. Because of inheritance and overriding of methods, a method in the dispatch table for class X may call a method in either a derived or inherited class. The amount of pointer adjustment depends on the class of the method being called as well as the class of the object on which the dispatch is made. This offset is best stored with the function pointer in the dispatch table. The general case works as follows. Say that A multiply-inherits from B1,...,Bn. The layout of the object will be: --------------------------------------------------------------------------------- | dp | B1's data | dp2 | B2's data | . . . | dpn | Bn's data | A's data | --------------------------------------------------------------------------------- | | | ------------------------- | | ------------------------- |-->| Bn's | | |-->| B2's | ------------------------- | ------------------------- | ------------------------------------- |-->| B1,B2,...,Bn,A's | ------------------------------------- The remaining problem is how to calculate the offsets. We illustrate the idea with an example: Say A inherits from B,C B has methods f,g C has methods h,j A has method k and overrides f and h with new definitions. let "c" be the offset from the beginning of the object to the beginning of the "C" sub-object. --------------------------------------------------- | dp1 | B's data | dp2 | C's data | A's data | --------------------------------------------------- | | ------------------------- | ---->| | | | ------------------------- | -------------------------------------------------------- |--->| < A::f,0> | | | | class myclass { T x; ... T f(T y) { ... } ... } A template provides type polymorphism in a way reminiscent of ML. Templates are also similar to the parameterized classes of Sather. The idea is that the template can be instantiated with any type T, giving a new class myclass. The syntax for instantiating a class is: myclass where T is a type. For example, a declaration of a variable z could be myclass z; Here is an example template (the code won't quite run as written): template class stack { T stack[100]; int top; public: void push(T& i) { if(top < 100) stack[top++] = i; } T pop() { return stack(top > 0 ? --top : top); } }; To make a stack of integers, we can declare stack z; A template can be parameterized by any number of classes. In addition, templates can be inherited just like other classes. For example, the stack template could be inherited and instantiated in another template: template class fancy_stack : stack { ... } Templates give C++ parameterized classes: a stack template can be instantiated to a stack of ints, a stack of windows, and so on. This kind of polymorphism is notoriously difficult to provide in OO languages, because the type checking becomes very difficult. How does C++ do it? The answer is that C++ doesn't really provide typing of templates at all. Templates have macro semantics: they are not checked themselves, only the results of an instantiation are checked. This makes type checking easy, because it reduces automatically to the type checking problem without templates: first expand away all templates and then perform type checking. In ML terms, this is equivalent to taking a let expression let x = e in e' and translating it into e'[ x <- e ] and then type checking the result of the substitution. What's the disadvantage of this approach? There are several: 1. A template is type checked many times, not just once. In theory this can have a very large negative impact on compiler performance. 2. Type checking is delayed from the place something is defined until it is used. This situation is undesirable and counter to the function of type checking. For example, a template shipped as part of a product may fail when a customer tries to use it because of an error in the template itself. 3. More generally, there is *no* type checking of templates. Only the uses are type checked! 4. The macro semantics means that a distinct copy of the code is generated for every distinct instantiation of the template. 5. Separate compilation is undermined. Huge amounts of code can be tucked in templates, inherited and modified in an arbitrary chain of dependencies. All of this must be recompiled for each use of the template. 6. The interface imposed by the template on its argument class is implicit. For example, the template may require that the parameter class have a method "foo", but the only way to learn that is to read the code (or the type errors produced by the compiler). Templates are not all bad. They are relatively simple to implement and will work fine so long as templates are small and uncomplicated. But they will not scale well to very large parameterized classes.