13. Class Concept Refinements

Say what you mean, simply and directly.

Brian Kernighan

Abstract classes — virtual functions and constructors — const members functions — refinement of the const concept — static member functions — nested classes — the inherited:: proposal — relaxation of the overriding rules — multi-methods — protected members — virtual function table allocation — pointers to members.

13.1 Introduction

Because classes are so central in C++, I receive a steady stream of requests for modifications and extensions of the class concept. Almost all requests for modifications must be rejected to preserve existing code, and most suggestions for extensions have been rejected as unnecessary, impractical, not fitting with the rest of the language, or simply “too difficult to handle just now.” Here, I present a few refinements that I felt essential to consider in detail and in most cases to accept. The central issue is to make the class concept flexible enough to allow techniques to be expressed within the type system without casts and other low-level constructs.

13.2 Abstract Classes

The very last feature added to Release 2.0 before it shipped was abstract classes. Late modifications to releases are never popular, and late changes to the definition of what will be shipped are even less so. My impression was that several members of management thought I had lost touch with the real world when I insisted on this feature. Fortunately, Barbara Moo was willing to back up my insistence that abstract classes were so important that they ought to ship now rather than being delayed for another year or more.

An abstract class represents an interface. Direct support for abstract classes

– helps catch errors that arise from confusion of classes’ role as interfaces and their role in representing objects;

– supports a style of design based on separating the specification of interfaces and implementations.

13.2.1 Abstract Classes for Error Handling

Abstract classes directly address a source of errors [Stroustrup, 1989b]:

"One of the purposes of static type checking is to detect mistakes and inconsistencies before a program is run. It was noted that a significant class of detectable errors was escaping C++’s checking. To add insult to injury, the language actually forced programmers to write extra code and generate larger programs to make this happen.

Consider the classic “shape” example. Here, we must first declare a class shape to represent the general concept of a shape. This class needs two virtual functions rotate() and draw(). Naturally, there can be no objects of class shape, only objects of specific shapes. Unfortunately C++ did not provide a way of expressing this simple notion directly.

The C++ rules specify that virtual functions, such as rotate() and draw(), must be defined in the class in which they are first declared. The reason for this requirement is to ensure that traditional linkers can be used to link C++ programs and to ensure that it is not possible to call a virtual function that has not been defined. So the programmer writes something like this:

class shape {
    point center;
    color col;
    // ...
public:
    point where() { return center; }
    void move(point p) { center=p; draw(); }
    virtual void rotate(int)
        { error("cannot rotate"); abort(); }
    virtual void draw()
        { error("cannot draw"); abort(); }
    // ...

};

This ensures that innocent errors such as forgetting to define a draw() function for a class derived from shape and silly errors such as creating a “plain” shape and attempting to use it cause run-time errors. Even when such errors are not made, memory can easily get cluttered with unnecessary virtual tables for classes such as shape and with functions that are never called, such as draw() and rotate(). The overhead for this can be noticeable.

The solution is simply to allow the user to say that a virtual function does not have a definition; that is, it is a “pure virtual function.” This is done by an initializer = 0:

class shape {
    point center;
    color col;
    // ...
public:
    point where() { return center; }
    void move(point p) { center=point; draw(); }
    virtual void rotate(int) =0;   // pure virtual
    virtual void draw() =0;        // pure virtual
    // ...
};

A class with one or more pure virtual functions is an abstract class. An abstract class can be used only as a base for another class. In particular, it is not possible to create objects of an abstract class. A class derived from an abstract class must either define the pure virtual functions from its base or again declare them to be pure virtual functions.

The notion of pure virtual functions was chosen over the idea of explicitly declaring a class to be abstract because the selective definition of functions is much more flexible.”

As shown, it was always possible to represent the notion of an abstract class in C++; it just involved a little more work than one would like. It was also understood by some to be an important issue (for example, see [Johnson, 1989]). However, not until a few weeks before the release date for 2.0 did it dawn on me that only a small fraction of the C++ community had actually understood the concept. Further, I realized that lack of understanding of the notion of abstract classes was the source of many problems that people experienced with their designs.

13.2.2 Abstract Types

A common complaint about C++ was (and is) that private data is included in the class declaration, so when a class’ private data is changed, code using that class must be recompiled. Often, this complaint is expressed as “abstract types in C++ aren’t really abstract” and “the data isn’t really hidden.” What I hadn’t realized was that many people thought that because they could put the representation of an object in the private section of a class declaration then they actually had to put it there. This is clearly wrong (and that is how I failed to spot the problem for years). If you don’t want a representation in a class, don’t put it there! Instead, delay the specification of the representation to some derived class. The abstract class notation allows this to be made explicit. For example, one can define a set of T pointers like this:

class set {
public:
    virtual void insert(T*) = 0;
    virtual void remove(T*) =0;
    
    virtual int is_member(T*) =0;

    virtual T* first() =0;
    virtual T* next() = 0;
    
    virtual ~set() { }
};

This provides all the information that people need to use a set. More importantly in this context, it contains no representation or other implementation details. Only people who actually create objects of set classes need to know how those sets are represented. For example, given:

class slist_set: public set, private slist {
    slink* current_elem;
public:
    void insert(T*);
    void remove(T*);

    int is_member(T*);
    
    T* first();
    T* next();

    slist_set(): slist (), current_elem(0) { }
};

we can create slist_set objects that can be used as sets by users who have never heard of an slist_set. For example:

void user1(set& s)
{
    for (T* p = s.first(); p; p=s.next()) {
        // use p
    }
}

void user2()
{
    slist_set ss;
    // . . .
    userl(ss);
}

Importantly, a user of the abstract class set, such as user1(), can be compiled without including the headers defining slist_set and the classes, such as slist, that it in turn depends on.

As mentioned, attempts to create objects of an abstract class are caught at compile time. For example:

void f(set& s1)    // fine
{

    set s2;        // error: declaration of object
                   //        of abstract class set.
    set* p = 0;    // fine
    set& s3 = s1;  // fine
}

The importance of the abstract class concept is that it allows a cleaner separation between a user and an implementer than is possible without it. An abstract class is purely an interface to the implementations supplied as classes derived from it. This limits the amount of recompilation necessary after a change as well as the amount of information necessary to compile an average piece of code. By decreasing the coupling between a user and an implementer, abstract classes provide an answer to people complaining about long compile times and also serve library providers, who must worry about the impact on users of changes to a library implementation. I have seen large systems in which the compile times were reduced by a factor of ten by introducing abstract classes into the major subsystem interfaces. I had unsuccessfully tried to explain these notions in [Stroustrup, 1986b]. With an explicit language feature supporting abstract classes I was much more successful [2nd].

13.2.3 Syntax

The curious = 0 syntax was chosen over the obvious alternative of introducing a keyword pure or abstract because at the time I saw no chance of getting a new keyword accepted. Had I suggested pure, Release 2.0 would have shipped without abstract classes. Given a choice between a nicer syntax and abstract classes, I chose abstract classes. Rather than risking delay and incurring the certain fights over pure, I used the traditional C and C++ convention of using 0 to represent “not there.” The = 0 syntax fits with my view that a function body is the initializer for a function and also with the (simplistic, but usually adequate) view of the set of virtual functions being implemented as a vector of function pointers (§3.5.1). In fact, =0 is not best implemented by putting a 0 in the vtbl. My implementation places a pointer to a function called __pure_virtual_called in the vtbl; this function can then be defined to give a reasonable run-time error.

I chose a mechanism for specifying individual functions pure rather than a way of declaring a complete class abstract because the pure virtual function notion is more flexible. I value the ability to define a class in stages; that is, I find it useful to define some virtual functions and leave the definition of the rest to further derived classes.

13.2.4 Virtual Functions and Constructors

The way an object is constructed out of base classes and member objects (§2.11.1) has implications on the way virtual functions work. Occasionally, people have been confused and even annoyed by some of these implications. Let me therefore try to explain why I consider the way C++ works in this respect almost necessary.

13.2.4.1 Calling a Pure Virtual Function

How can a pure virtual function – rather than a derived class function overriding it -ever be called? Objects of an abstract class can only exist as bases for other classes. Once the object for the derived class has been constructed, a pure virtual function has been defined by an overriding function from the derived class. However, during construction it is possible for the abstract class’ own constructor to call a pure virtual function by mistake:

class A {
public:
    virtual void f() = 0;
    void g();
    A();

};


A::A()
{
     f();       // error: pure virtual function called
     g();       // looks innocent enough
}

The illegal call of A::f() is easily caught by the compiler. However, A::g() may be declared like this

void A::g() { f(); }

in some other translation unit. In that case, only a compiler that does crosscompilation-unit analysis can detect the error, and a run-time error is the alternative.

13.2.4.2 Base-first Construction

I strongly prefer designs that do not open the possibility of run-time errors to those that do. However, I don’t see the possibility of making programming completely safe. In particular, constructors create the environment in which other member functions operate (§2.11.1). While that environment is under construction, the programmer must be aware that fewer guarantees can be made. Consider this potentially confusing example:

class B {
public:
    int b;
    virtual void f();
    void g();
    // ...
    B();
};

class D:public B {
public:
    X x;
    void f();
    // ...
    D();
};

B::B()
{
     b++;    // undefined:B::b isn't yet initialized.
     f();    // calls:B::f(); not D::f().
}

A compiler can easily warn about both potential problems. If you really mean to call B’s own f() say so explicitly: B::f().

The way this constructor behaves contrasts with the way an ordinary member function can be written (relying on the proper behavior of the constructor):

void B::g()
{
   b++;   // fine, since B::b is a member
          //       B::B should have initialized it.
   f();   // calls:D::f() if B::g is called for a D.
}

The difference in the function invoked by f() in B::B() and B::g() when invoked for a B part of a D can be a surprise to novices.

13.2.4.3 What if?

However, consider the implication of the alternative, that is, to have every call of the virtual function f() invoke the overriding function:

void D::f()
{
    // operation relying on D::X having been properly
    // initialized by D::D
}

If an overriding function could be called during construction, then no virtual function could rely on proper initialization by constructors. Consequently, every overriding function would have to be written with the degree of resilience (and paranoia) usually reserved for constructors. Actually, writing an overriding function would be worse than writing a constructor because in a constructor it is relatively easy to determine what has and hasn’t yet been initialized. In the absence of the guarantee that the constructor has been run, the writer of an overriding function would always have two choices:

[1] Simply hope/assume that all necessary initializations have been done.

[2] Try to guard against uninitialized bases and members.

The first alternative makes constructors unattractive. The second alternative becomes truly unmanageable because a derived class can have many direct and indirect base classes and because there is no run-time check that you can apply to an arbitrary variable to see if it has been initialized.

void D::f() // nightmare (not C++)
{
    if (base_initialized) {
        // operation relying on D::X having
        // been initialized by D::D
    }
    else {
         // do what can be done without relying
         // on D::X having been initialized
    
   }

}

Consequently, had constructors called overriding functions, the use of constructors would have had to be severely restricted to allow reasonable coding of overriding functions.

The basic design point is that until the constructor for an object has run to completion the object is a bit like a building during construction: You have to suffer the inconveniences of a half-completed structure, often rely on temporary scaffolding, and take precautions commensurate with the more hazardous environment. In return, compilers and users are allowed to assume that an object is usable after construction.

13.3 const Member Functions

In Cfront 1.0, “constness” had been incompletely enforced and when I tightened up the implementation, we found a couple of holes in the language definition. We needed a way to allow a programmer to state which member functions update the state of their object and which don’t:

class X {
    int aa;
public:
    void update() { aa++; }
    int value() const { return aa; }
    void cheat() const { aa++; } // error: *this is const
};

A member function declared const, such as X::value(), is called a const member function and is guaranteed not to change the value of an object. A const member function can be used on both const and non-const objects, whereas non-const member functions, such as X::update(), can only be called for non-const objects:

int g(X ol, const X& o2)
{
    01. update();      // fine
    02. update();      // error: o2 is const
    return ol.value() + o2.value(); // fine
}

Technically, this behavior is achieved by having the this pointer point to an X in a non-const member function of X and point to a const X in a const member function of X.

The distinction between const and non-const functions allows the useful logical distinction between functions that modify the state of an object and functions that don’t to be directly expressed in C++. Const member functions were among the language features that received a significant boost from the discussions at the Estes Park implementers workshop (§7.1.2).

13.3.1 Casting away const

As ever, C++ was concerned with the detection of accidental errors, rather than with the prevention of fraud. To me, that implied that a function should be allowed to "“cheat” by “casting away const.” It was not considered the compiler’s job to prevent the programmer from explicitly subverting the type system. For example [Stroustrup, 1992b]:

“It is occasionally useful to have objects that appear as constants to users but do in fact change their state. Such classes can be written using explicit casts:

class XX {
    int a;
    int calls_of_f;
    int f() const { ((XX*)this)->calls_of_f++; return a; }
    // . . .
};

The explicit type conversion indicates that something is not quite right. Changing the state of a const object can be quite deceptive, is error-prone in some contexts, and won’t work if the object is in read-only memory. It is often better to represent the variable part of such an object as a separate object:

class XXX {
    int a;
    int& calls_of_f;
    int f() const { calls_of_f++; return a; }
    // ...
    XXX() : calls_of_f(*new int) { /* ... */ }
    ~XXX() { delete &calls_of_f; /*...*/}
    // ...
}

This reflects that the primary aim of const is to specify interfaces rather than to help optimizers, and also the observation that though the freedom/flexibility is occasionally useful it can be misused.”

The introduction of const_cast14.3.4) enables programmers to distinguish casts intended to “cast away const” from casts intended to do other forms of type manipulation.

13.3.2 Refinement of the Definition of const

To ensure that some, but not all, const objects could be placed in read-only memory (ROM), I adopted the rule that any object that has a constructor (that is, required runtime initialization) can’t be placed in ROM, but other const objects can. This ties in to a long-running concern of what can be initialized and how and when. C++ provides both static (link time) and dynamic (run-time) initialization (§3.11.4) and this rule allows run-time initialization of const objects while still allowing for the use of ROM for objects that don’t require run-time initialization. The typical example of the latter is a large array of simple objects, such as a YACC parser table.

Tying the notion of const to constructors was a compromise between my ideal for const, realities of available hardware, and the view that programmers should be trusted to know what they are doing when they write an explicit type conversion. At the initiative of Jerry Schwarz, this rule has now been replaced by one that more closely reflects my original ideal. An object declared const is considered immutable from the completion of its constructor until the start of its destructor. The result of a write to the object between those points is deemed undefined.

When originally designing const, I remember arguing that the ideal const would be an object that is writable until the constructor had run, then becomes readonly by some hardware magic, and finally upon the entry into the destructor becomes writable again. One could imagine a tagged architecture that actually worked this way. Such an implementation would cause a run-time error if someone attempted to write to an object defined const. On the other hand, someone could write to an object not defined const that had been passed as a const reference or pointer. In both cases, the user would have to cast away const first. The implication of this view is that casting away const for an object that was originally defined const and then writing to it is at best undefined, whereas doing the same to an object that wasn’t originally defined const is legal and well defined.

Note that with this refinement of the rules, the meaning of const doesn’t depend on whether a type has a constructor or not; in principle, they all do. Any object declared const now may be placed in ROM, be placed in code segments, be protected by access control, etc., to ensure that it doesn’t mutate after receiving its initial value. Such protection is not required, however, because current systems cannot in general protect every const from every form of corruption.

An implementation still retains a large degree of discretion over how a const is managed. There is no logical problem in having a garbage collector or a database system change the value of a const object (say, moving it to disk and back) as long as it ensures that the object appears unmodified to a user.

13.3.3 Mutable and Casting

Casting away const is still objectionable to some because it is a cast, and even more so because it is not guaranteed to work in all cases. How can we write a class like XX from §13.3.1 that doesn’t require casting and doesn’t involve an indirection as in class XXX? Thomas Ngo suggested that it ought to be possible to specify that a member should never be considered const even if it is a member of a const object. This proposal was kicked around in the committee for years until Jerry Schwarz successfully championed a variant to acceptance. Originally ~const was suggested as a notation for “can’t ever be const.” Even some of the proponents of the notion considered that notation too ugly, so the keyword mutable was introduced into the proposal that the ANSI/ISO committee accepted:

class XXX {
    int a;
    mutable int cnt; // cnt will never be const
public:
    int f() const { cnt++; return a; }
    
    // ...
};
XXX var;          // var.cnt is writable (of course)

const XXX cnst;  // cnst.cnt is writable because
                 // XXX::cnt is declared mutable

The notion is still somewhat untried. It does reduce the need for casts in real systems, but not as much as some people hoped for. Dag Brück and others reviewed considerable amounts of real code to see which casts were casting away const and which of those could be eliminated using mutable. This study confirmed the conclusion that “casting away const” cannot be avoided in general (§14.3.4) and that mutable appears to eliminate casting away const in less than half of the cases where it is needed in the absence of mutable. The benefits of mutable appear to be very dependent on programming style. In some cases, every cast could be eliminated by using mutable; in others, not a single cast could be eliminated.

Some people had expressed hope that a revised const notion plus mutable would open the door to significant new optimizations. This doesn’t appear to be the case. The benefits are largely in code clarity and in increasing the number of objects that can have their values precomputed so that they can be placed in ROM, code segments, etc.

13.4 Static Member Functions

A static data member of a class is a member for which there is only one copy rather than one per object. Consequently, a static member can be accessed without referring to any particular object. Static members are used to reduce the number of global names, to make obvious which static objects logically belong to which class, and to be able to apply access control to their names. This is a boon for library providers since it avoids polluting the global namespace and thereby allows easier writing of library code and safer use of multiple libraries.

These reasons apply to functions as well as objects. In fact, most of the names a library provider wants non-global are function names. I observed that nonportable code, such as ((X*)0) ->f(), was used to simulate static member functions. This trick is a time bomb because sooner or later someone will declare an f() called this way to be virtual. Then, the call will fail horribly because there is no X object at address zero. Even when f() is not virtual, such calls will fail under some implementations of dynamic linking.

At a course I was giving for EUUG (the European UNIX Users’ Group) in Helsinki in 1987, Martin O’Riordan pointed out to me that static member functions were an obvious and useful generalization. That was probably the first mention of the idea. Martin was working for Glockenspiel in Ireland at the time and later went on to become the main architect of the Microsoft C++ compiler. Later, Jonathan Shopiro championed the idea and made sure it didn’t get lost in the mass of work for Release 2.0.

A static member function is a member so that its name is in the class scope, and the usual access control rules apply. For example:

class task {
    // ...
    static task* chain;
public:
    static void schedule(int);
    // ...
};

A static member declaration is only a declaration and the object or function it declares must have a unique definition somewhere in the program. For example:

task* task::chain = 0;
void task::schedule(int p) {/*...*/}

A static member function is not associated with any particular object and need not be called using the special member function syntax. For example:

void f(int priority)
{
    // ...
    task::schedule(priority);
    // ...
}

In some cases, a class is used simply as a scope in which to put otherwise global names as static members so they don’t pollute the global namespace. This is one of the origins of the notion of namespaces (§17).

Static member functions were among the language features that received a significant boost from the discussions at the Estes Park implementers workshop (§7.1.2).

13.5 Nested Classes

As mentioned in §3.12, nested classes were reintroduced into C++ by the ARM. This made the scope rules more regular and improved the facilities for localization of information. We could now write:

class String {
    class Rep {
        // ...
    };
    Rep* p; // String is a handle to Rep
    static int count;
    // ...
public:
    char& operator[] (int i) ;
    // ...
};

to keep the Rep class local. Unfortunately, this led to an increase in the amount of information placed in class declarations and consequently to an increase in compile times and in the frequency of recompilations. Too much interesting and occasionally changing information was put into nested classes. In many cases, such information was not really of interest to the users of classes such as String and should therefore be put elsewhere along with other implementation details. Tony Hansen proposed to allow forward declaration of a nested class in exact parallel to the way member functions and static members are handled:

// file String.h (the interface):

    class String {
        class Rep;
        Rep* p; // String is a handle to Rep
        static int count;
        // ...
    public :
        char& operator[] (int i) ;
        // ...
};

// file String.c (the implementation):
    
    class String::Rep {
        // ...
    };


    static int String::count = 1;


    char& String::operator[](int i)
    {
        // ...
    }

This extension was accepted as something that simply corrected an oversight. The technique it supports shouldn’t be underestimated, though. People still load up their header files with all kinds of unnecessary stuff and suffer long compile times in consequence. Therefore, every technique and feature that helps reduce unnecessary coupling between users and implementers is important.

13.6 Inherited::

At one of the early standards meetings Dag Brück submitted a proposal for an extension that several people had expressed interest in [Stroustrup, 1992b]:

“Many class hierarchies are built “incrementally,” by augmenting the behavior of the base class with added functionality of the derived class. Typically, the function of the derived class calls the function of the base class, and then performs some additional operations:

struct A { virtual void handle(int); };
struct D:A { void handle(int); };

void D::handle(int i)
{
   A::handle(i);
   // other stuff
}

The call to handle() must be qualified to avoid a recursive loop. The example could with the proposed extension be written as follows:

void D::handle(int i)
{
   inherited::handle(i);
   // other stuff
}

Qualifying by the keyword inherited can be regarded as a generalization of qualifying by the name of a class. It solves a number of potential problems of qualifying by a class name, which is particularly important for maintaining class libraries.”

I had considered this early on in the design of C++, but had rejected it in favor of qualification with the base class name because that solution could handle multiple inheritance, and inherited:: clearly can’t. However, Dag observed that the combination of the two schemes would deal with all problems without introducing loopholes:

“Most class hierarchies are developed with single inheritance in mind. If we change the inheritance tree so class D is derived from both A and B, we get:

struct A { virtual void handle(int); };
struct B { virtual void handle(int); };
struct D:A, B { void handle(int); };

void D::handle(int i)
{
   A::handle(i);                 // unambiguous
   inherited::handle(i);         // ambiguous
}

In this case A::handle() is legal C++ and possibly wrong. Using inherited::handle() is ambiguous here, and causes an error message at compile time. I think this behavior is desirable, because it forces the person merging two class hierarchies to resolve the ambiguity. On the other hand, this example shows that inherited may be of more limited use with multiple inheritance.”

I was convinced by these arguments and by the meticulous paperwork that documented its details. Here was a proposal that was clearly useful, easily understood, and trivial to implement. It also had genuine experience behind it since a variant of it had been implemented by Apple based on their experiences with Object Pascal. It is also a variant of the Smalltalk super.

After the final discussion of this proposal in the committee Dag volunteered it for use as a textbook example of a good idea that shouldn’t be accepted [Stroustrup, 1992b]:

The proposal is well-argued and – as is the case with most proposals – there was more expertise and experience available in the committee itself. In this case the Apple representative had implemented the proposal. During the discussion we soon agreed that the proposal was free of major flaws. In particular, in contrast to earlier suggestions along this line (some as early as the discussions about multiple inheritance in 1986) it correctly dealt with the ambiguities that can arise when multiple inheritance is used. We also agreed that the proposal was trivial to implement and would in fact be helpful to programmers.

Note that this is not sufficient for acceptance. We know of dozens of minor improvements like this and at least a dozen major ones. If we accepted all the language would sink under its own weight (remember the Vasa!). We will never know if this proposal would have passed, though, because at this point in the discussion, Michael Tiemann walked in and muttered something like “but we don’t need that extension; we can write code like that already.” When the murmur of “but of course we can’t!” had died down Michael showed us how:

class foreman:public employee {
    typedef employee inherited;
    // ...
    void print();

};

class manager:public foreman {
    typedef foreman inherited;
    // ...
    void print();
};

void manager::print()
{
    inherited::print();
    // ...
}

A further discussion of this example can be found in [2nd,pp205]. What we hadn’t noticed was that the reintroduction of nested classes into C++ had opened the possibility of controlling the scope and resolution of type names exactly like other names.

Given this technique, we decided that our efforts were better spent on some other standards work. The benefits of inherited:: as a built-in facility didn’t sufficiently outweigh the benefits of what the programmer could do with existing features. In consequence, we decided not to make inherited:: one of the very few extensions we could afford to accept for C++.”

13.7 Relaxation of Overriding Rules

Consider writing a function that returns a copy of an object. Assuming a copy constructor, this is trivially done like this:

class B {
public:
    virtual B* clone() { return new B(*this); }
    // ...
};

Now any object of a class derived from B that overrides B::clone can be correctly cloned. For example:

class D : public B {
public:
        // old rule:
        // clone() must return a B* to override B::clone():
    B* clone()  { return new D(*this); }

    void h();
    // ...
};

void f(B* pb, D* pd)
{
    B* pb1 = pb->clone();
    B* pb2 = pd->clone(); // pb2 points to a D
    // ...
}

Unfortunately, the fact that pd points to a D (or something derived from D) is lost:

void g(D* pd)
{
    B* pb1 = pd->clone(); // ok
    D* pdl = pd->clone(); // error: clone() returns a B*
    pd->clone()->h();     // error: clone() returns a B*

    // ugly workarounds:

    D* pd2 = (D*)pd->clone();
    ((D*)pd->clone())->h();
}

This proved a nuisance in real code, and several people observed that the rule that an overriding function must have exactly the same type as the overridden could be relaxed without opening the hole in the type system or imposing serious implementation complexity. For example, this might be allowed:

Class D:public B {
public:
       // note, clone() returns a D*:
    D* clone () { return new D(*this); }

    void h();
    // ...
};

void gg(B* pb, D* pd)
{
   B* pb1 = pd->clone (); // ol
   D* pb1 = pd->clone (); // ol
   pd->clone ()->h ();    // ol

   D* pd2 = pb->clone(); // error (as always)
   pb->clone()->h();     // error (as always)
}

This extension was originally proposed by Alan Snyder and happens to be the first extension ever to be officially proposed to the committee. It was accepted in 1992.

Two questions had to be asked before we could accept it:

[1] Were there any serious implementation problems (say, in the area of multiple inheritance or pointers to members)?

[2] Out of all the conversions that could possible be handled for return types of overriding functions which – if any – are worthwhile?

Personally, I didn’t worry much about [1] because I thought I knew how to implement the relaxation in general, but Martin O’Riordan did worry and produced papers for the committee demonstrating implementability in detail.

My main problem was to try to determine whether this relaxation was worthwhile and for exactly which set of conversions? How common is the need for virtual functions called for an object of a derived type and needing operations to be performed of a return value of that derived type? Several people, notably John Bruns and Bill Gibbons, argued strongly that the need was common and not restricted to a few computer science examples such as clone. The data that finally convinced me was the observation by Ted Goldstein that almost two thirds of all casts in a multi-100,000-line system he was involved in at Sun were workarounds that would be eliminated by this relaxation of the overriding rules. In other words, what I find most attractive is that the relaxation allows people to do something important within the type system instead of using casts. This brought the relaxation of return types for overriding functions into the mainstream of my effort to make C++ programming safer, simpler, and more declarative. Relaxing the overriding rule would not only eliminate many ordinary casts, but also remove one temptation for misuse of the new dynamic casts that were being discussed at the same time as this relaxation (§14.2.3).

After some consideration of the alternatives, we decided to allow overriding of a B* by a D* and of a B& by a D& where B is an accessible base of D. In addition, const can be added or subtracted wherever that is safe. We decided not to relax the rules to allow technically feasible conversions such as a D to an accessible base B, a D to an X for which D has a conversion, int* to void*, double to int, etc. We felt that the benefits from allowing such conversions through overriding would not outweigh the implementation cost and the potential for confusing users.

13.7.1 Relaxation of Argument Rules

One major reason that I had been suspicious about relaxing the overriding rules for return types was that in my experience it invariably had been proposed together with an unacceptable “equivalent” relaxation for argument types. For example:

class Fig {
public:
    virtual int operator==(const Fig&);
    // ...
};


class ColFig:public Fig {
public:
    // Assume that Coifig::operator==()
    // overrides Fig::operator==()
   //  (not allowed in C++).

    int operator==(const ColFig& x) ;
    // ...
private:
    Color col;
};

int ColFig::operator==(const ColFig& x)
{
    return col == x.col && Fig::operator==(x);
}

This looks very plausible and allows useful code to be written. For example:

void f(Fig& fig, ColFig& cf1, ColFig& cf2)
{
    if (fig==cf1) { // compare Figs
        // ...
    } else if (cf1==cf2) { // compare ColFigs
        // ...
    }
}

Unfortunately, this also leads to an implicit violation of the type system:

void g(Fig& fig, ColFig& cf)
{
    if (cf==fig) {// compare what?
        // ...
    }
}

If ColFig::operator==() overrides Fig::operator—() then cf==fig will invoke ColFig::operator==() with a plain Fig argument. This would be a disaster because ColFig::operator—() accesses the member col, and Fig does not have such a member. Had ColFig::operator — () written to its argument, memory corruption would have resulted. I had considered this scenario when I first designed the rules for virtual functions and deemed it unacceptable.

Consequently, had this overriding been allowed, a run-time check would have been needed for every argument to a virtual function. Optimizing these tests away would not be easy. In the absence of global analysis, we never know if an object might originate in some other file and thus possibly be of a type that did a dangerous overriding. The overhead of this checking is unattractive. Also, if every virtual function call became a potential source of exceptions, users would have to prepare for those. That was considered unacceptable.

The alternative for the programmer is to explicitly test when a different kind of processing is needed for an argument of a derived class. For example:

class Figure {
public:
    virtual int operator==(const Figure&);
    // ...
};

class ColFig:public Figure {
public:
    int operator==(const Figure& x) ;
    // ...
private:
    Color col;
};

int ColFig::operator==(const Figure& x)
{
    if (Figure::operator==(x)) {
        const ColFig* pc = dynamic_cast<const ColFig*>(&x);
        if (pc) return col == pc->col;
    }
    return 0;
}

In this way, the run-time checked cast dynamic_cast14.2.2) is the complement to the relaxed overriding rules. The relaxation safely and declaratively deals with return types; the dynamic_cast operator explicitly and relatively safely deals with argument types.

13.8 Multi-methods

I repeatedly considered a mechanism for a virtual function call based on more than one object, often called multi-methods. I rejected multi-methods with regret because I liked the idea, but couldn’t find an acceptable form under which to accept it. Consider:

class Shape {
    // ...
};

class Rectangle: public Shape {
    // ...
};

class Circle: public Shape {
    // ...
};

How would I design an intersect() that is correctly called for both of its arguments? For example,

void f(Circles c, Shapes s1, Rectangles r, Shapes s2)
{
    intersect(r,c);
    intersect(c,r);
    intersect(c,s2);
    intersect(s1,r);
    intersect(r,s2);
    intersect(s1, c);
    intersect(s1,s2);
}

If r and s refers to a Circle and a Shape, respectively, we would like to implement intersect by four functions:

bool intersect(const Circles, const Circles);
bool intersect(const Circles, const Rectangles);
bool intersect(const Rectangles, const Circles);
bool intersect(const Rectangles, const Rectangles);

Each call ought to call the right function in the same way a virtual function does. However, the right function must be selected based on the run-time type of both arguments. The fundamental problems, as I saw them, were to find

[1] A calling mechanism that was as simple and efficient as the table lookup used for virtual functions.

[2] A set of rules that allowed ambiguity resolution to be exclusively a compiletime matter.

I don’t consider the problem unsolvable, but I have never found this issue pressing enough to reach the top of my stack of pending issues for long enough to work out the details of a solution.

One worry I had was that a fast solution seemed to require a lot of memory for the equivalent of a virtual function table, whereas anything that didn’t “waste” a lot of space by replicating table entries would be slow, have unpredictable performance characteristics, or both. For example, any implementation of the Circle-and-Rectangle example that doesn’t involve a run-time search for the function to invoke seems to require four pointers to functions. Add an extra class Triangle, and we seem to need nine pointers to functions. Derive a class Smiley from Circle and we seem to need sixteen, though we should be able to save the last seven entries by using entries involving Circle for all Smileys.

Worse, the arrays of pointers to functions that would be equivalent to virtual function tables could not be composed until the complete program was known, that is, by the linker. The reason is that there is no one class to which all overriding functions belong. There couldn’t be such a class exactly because any interesting overriding function will depend on two or more argument types. At the time, this problem was unsolvable because I was unwilling to have a language feature that depended on nontrivial linker support. Experience had taught me that such support would not be available for years.

Another problem that bothered me, though it didn’t seem unsolvable, was how to handle ambiguities. The obvious answer is that calls of multi-methods must obey exactly the same ambiguity rules as other calls. However, this answer was obscured for me because I was looking for a special syntax and special rules for calling multimethods. For example:

(r@s)->intersect(); // rather than intersect(r,s)

This was a dead end.

Doug Lea suggested a much better solution [Lea, 1991]: Allow arguments to be explicitly declared virtual. For example:

bool intersect(virtual const Shapes, virtual const Shapes);

A function that matches in name and in argument types using a relaxed matching rule along the lines adopted for the return type overrides. For example:

bool intersect(const Circles, const Rectangles) // overrides
{
    // ...
}

Finally, multi-methods can be called with the usual call syntax exactly as shown above.

Multi-methods is one of the interesting what-ifs of C++. Could I have designed and implemented them well enough at the time? Would their applications have been important enough to warrant the effort? What other work might have been left undone to provide the time to design and implement multi-methods? Since about 1985, I have always felt some twinge of regret for not providing multi-methods. To wit: The only official talk I ever gave at an OOPSLA conference was part of a panel making a statement against language bigotry and pointless “religious” language wars [Stroustrup,1990]. I presented some bits of CLOS that I particularly liked and emphasized multi-methods.

13.8.1 Workarounds for Multi-methods

So how do we write functions such as intersect() without multi-methods?

Until the introduction of run-time type identification (§14.2) the only support for resolution based on type at run time was virtual functions. Since we wanted to resolve based on two arguments we somehow needed two virtual function calls. For the Circle and Rectangle example above, there are three possible static argument types for a call, so we can provide three different virtual functions:

class Shape {
    // ...
    virtual bool intersect(const Shape&) const =0;
    virtual bool intersect(const Rectangles) const =0;
    virtual bool intersect(const Circles) const =0;
};

The derived classes override the virtual functions appropriately:

class Rectangle: public Shape {
    // ...
    bool intersect(const Shapes) const;
    bool intersect(const Rectangles) const;
    bool intersect(const Circles) const;
};

Any call of intersect() will resolve to the appropriate Rectangle or Circle function. We then have to ensure that the functions taking a nonspecific Shape argument use a second virtual function call to resolve that argument to a more specific one:

bool Rectangle::intersect(const Shapes s) const
{
    return s.intersect(*this); // *this is a Rectangle:
                               // resolve on s

}

bool Circle::intersect(const Shapes s) const
{
   return s.intersect(*this); // *this is a Circle:
                              // resolve on s
}

The other intersect() functions simply do their job on two arguments of known types. Note that only the first Shape::intersect() function is necessary for this technique. The other two Shape: :intersect() functions are an optimization that can be done where a derived class is known when the base class is designed.

This technique is called double dispatch and was first presented in [Ingalls, 1986]. In the context of C++, double dispatch has the weakness that adding a class to a hierarchy requires changes to existing classes. A derived class such as Rectangle must know about all of its sibling classes to include the right set of virtual functions. For example, adding class Triangle requires changes to both Rectangle, Circle, and – if the optimization used above is desired – also Shape:

class Rectangle: public Shape {
    // ...
    bool intersect(const Shapes);
    bool intersect(const Rectangles);
    bool intersect(const Circles);
    bool intersect(const Triangles);
};

Basically, in C++ double dispatch is a reasonably efficient and reasonably elegant technique for navigating hierarchies where one can modify class declarations to accommodate new classes and where the set of derived classes doesn’t change too often.

Alternative techniques involve storing some kind of type identifier in objects and selecting functions to be called based on those. Use of typeid() for run-time type identification (§14.2.5) is simply one example of this. One can maintain a data structure containing pointers to functions and use the type identifier to access that structure. This has the advantage that the base class doesn’t have to have any knowledge of which derived classes exist. For example, with suitable definitions

bool intersect(const Shape* s1, const Shape* s2)
{
   int i = find_index(si.type_id(),s2.type_id());
   if (i < 0) error("bad_index");
   extern Fct_table* tbl;
   Fct f = tbl[i];
   return f (si,s2);
}

will call the right function for every possible type of the two arguments. Basically, this manually implements the multi-method virtual function table hinted at above.

The relative ease with which each specific example of multi-methods can be simulated is a major reason that multi-methods never stayed at the top of my to-do list long enough to be worked out in detail. In a very real sense, this technique is the same as the one used to simulate virtual functions in C. Such workarounds are acceptable if they are needed only infrequently.

13.9 Protected Members

The simple private/public model of data hiding served C++ well where C++ was used essentially as a data abstraction language and for a large class of problems where inheritance was used for object-oriented programming. However, when derived classes are used, there are two kinds of users of a class: derived classes and “the general public.” The members and friends that implement the operations on the class operate on the class objects on behalf of these users. The private/public mechanism allows the programmer to distinguish clearly between the implementers and the general public, but does not provide a way of catering specifically to derived classes.

Shortly after Release 1.0, Mark Linton stopped by my office and made an impassioned plea for a third level of access control to directly support the style used in the Interviews library (§8.4.1) being developed at Stanford. We coined the word protected to refer to members of a class that were “like public” to members of a class and its derived classes yet "like private” to anyone else.

Mark was the main architect of Interviews. He argued persuasively based on genuine experience and examples from real code that protected data was essential for the design of an efficient and extensible X windows toolkit. The alternative to protected data was claimed to be unacceptable inefficiency, unmanageable proliferation of inline interface functions, or public data. Protected data, and in general, protected members seemed the lesser evil. Also, languages claimed “pure” such as Smalltalk supported this – rather weak – notion of protection over the – stronger – C++ notion of private. I had written code where data was declared public simply to be usable from derived classes and had seen code where the friend notion had been clumsily misused to grant access to explicitly named derived classes.

These were good arguments and essentially the ones that convinced me to allow protected members. However, I regard “good arguments” with a high degree of suspicion when discussing programming. There seem to be “good arguments” for every possible language feature and every possible use of it. What we need is data. Without data and properly evaluated experience, we are like the Greek philosophers who argued brilliantly for several centuries, yet didn’t quite manage to determine the four (or maybe even five) fundamental substances from which they were sure everything in the universe was composed.

Five years or so later, Mark banned the use of protected data members in Interviews because they had become a source of bugs: “novice users poking where they shouldn’t in ways that they ought to have known better than.” They also seriously complicate maintenance: “now it would be nice to change this, do you think someone out there might have used it?” Barbara Liskov’s OOPSLA keynote [Liskov,1987] gives a detailed explanation of the theoretical and practical problems with access control based on the protected notion. In my experience, there have always been alternatives to placing significant amounts of information in a common base class for derived classes to use directly. In fact, one of my concerns about protected is exactly that it makes it too easy to use a common base the way one might sloppily have used global data.

Fortunately, you don’t have to use protected data in C++; private is the default in classes and is usually the better choice. Note that none of these objections are significant for protected member functions. I still consider protected a fine way of specifying operations for use in derived classes.

Protected members were introduced into Release 1.2. Protected base classes were first described in the ARM and provided in Release 2.1. In retrospect, I think that protected is a case where “good arguments” and fashion overcame my better judgement and my rules of thumb for accepting new features.

13.10 Improved Code Generation

To some people, the most important “feature” of Release 2.0 wasn’t a feature at all, but a simple space optimization. From the beginning, the code generated by Cfront tended to be pretty good. As late as 1992, Cfront generated the fastest running code in a benchmark used to evaluate C++ compilers on a SPARC. Except for the implementation of the return value optimization suggested in [ARM,§ 12.1c] in Release 3.0, there have been no significant improvements in Cfront’s code generation since Release 1.0. However, Release 1.0 wasted space because each compilation unit generated its own set of virtual function tables for all the classes used in that unit. This could lead to megabytes of waste. At the time (about 1984), I considered the waste necessary in the absence of linker support and asked for such support. By 1987, that linker support hadn’t materialized. Consequently, I rethought the problem and solved it by the simple heuristic of laying down the virtual function table of a class right next to the definition of its first non-pure virtual non-inline function. For example:

class X {
public:
    virtual void fl() { /* ... */ }
    void f2();
    virtual void f3() = 0;
    virtual void f4(); // first non-inline non-pure virtual
    // ...
};

// in some file:

    void X::f4() { /* ... */ }
    
    // Cfront will place X's virtual function table here

I chose that heuristic because it doesn’t require cooperation from the linker. The heuristic isn’t perfect because space is still wasted for classes that don’t have a non-inline virtual function, but the space taken up by virtual function tables ceased to be a practical problem. Andrew Koenig and Stan Lippman were involved in the discussion of the details of this optimization. Naturally, other C++ compilers can and do choose their own solutions to this problem to suit their environments and engineering tradeoffs.

As an alternative, we considered simply generating virtual function table definitions in every compilation unit and then having a pre-linker eliminate all but one. This, however, was not easy to do portably. It was also plain inefficient. Why generate all those tables just to waste time throwing most away later? Alternative strategies are available to people who are willing to supply their own linker.

13.11 Pointers to Members

Originally, there was no way of expressing the concept of a pointer to a member function in C++. This led to the need to “cheat” the type system in cases, such as error handling, where pointers to functions are traditionally used. Given

struct S {
    int mf(char*);
};

people wrote code like this:

typedef void (*PSmem)(S*,char*);

PSmem m = (PSmem)&S::mf;

void g(S* ps)
{
    m(ps,"Hello");
}

This only worked with a liberal sprinkling of explicit casts that never ought to have worked in the first place. It also relied on the assumption that a member function is passed its object pointer (“the this pointer”) as the first argument in the way Cfront implements it (§2.5.2).

I considered this unacceptable as early as 1983, but felt no urgency to fix it. I considered it to be a purely technical issue that had to be answered to close a hole in the C++ type system, but of little practical importance. After finishing Release 1.0, I finally managed to find some time to plug this hole, and Release 1.2 implemented the solution. As it happened, the advent of environments relying on callbacks as a primary communication mechanism made the solution to this problem crucial.

The term pointer to member is a bit misleading because a pointer to member is more like an offset, a value that identifies a member of an object. However, had I called it “offset” people would have made the mistaken assumption that a pointer to member was a simple index into an object and would also have assumed that some forms of arithmetic could be applied. This would have caused even more confusion than the term pointer to member, which was chosen because I designed the mechanism as a close syntactic parallel to C’s pointer syntax.

Consider the C/C++ function syntax in all its glory:

int f(char* p) {/*...*/ }  // define function.
int (*pf)(char*) = &f;     // declare and initialize
                           // pointer to function.
int i = (*pf)("hello");    // call through pointer.

Inserting S:: and p-> in the appropriate places, I constructed a direct parallel for member functions:

class S {
    // ...
    int mf(char*);
};

int S::mf(char*p) {/*...*/}    // define member function.
int (S::*pmf)(char*) = &S::mf; // declare and
                               // initialize pointer to
                               // member function.
S* p;
int i = (p->*pmf)("hello");    // call function through
                               // pointer and object.

Semantically and syntactically, this notion of pointer-to-member functions makes sense. All I needed to do was to generalize it to include data members and find an implementation strategy. The acknowledgment section of [Lippman,1988] has this to say:

“The design of the pointer to member concept was a cooperative effort by Bjarne Stroustrup and Jonathan Shopiro with many useful comments by Doug McIlroy. Steve Dewhurst contributed greatly to the redesign of the pointer to member implementation to cope with multiple inheritance.”

At the time I was fond of saying that we discovered pointers to members more than we designed it. Most of 2.0 felt that way.

For a long time, I considered pointers to data members an artifact of generalization rather than something genuinely useful. Again, I was proven wrong. In particular, pointers to data members has proven a useful way of expressing the layout of a C++ class in an implementation-independent manner [Hübel, 1992].

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.174.168