MFC Programmer's SourceBook : Thinking in C++
Bruce Eckel's Thinking in C++, 2nd Ed Contents | Prev | Next

Upcasting

Earlier in the chapter, you saw how an object of a class derived from ofstream has all the characteristics and behaviors of an ofstream object. In FName2.cpp, any ofstream member function could be called for an FName2 object.

The most important aspect of inheritance is not that it provides member functions for the new class, however. It’s the relationship expressed between the new class and the base class. This relationship can be summarized by saying, “The new class is a type of the existing class.”

This description is not just a fanciful way of explaining inheritance – it’s supported directly by the compiler. As an example, consider a base class called Instrument that represents musical instruments and a derived class called Wind. Because inheritance means that all the functions in the base class are also available in the derived class, any message you can send to the base class can also be sent to the derived class. So if the Instrument class has a play( ) member function, so will Wind instruments. This means we can accurately say that a Wind object is also a type of Instrument. The following example shows how the compiler supports this notion:

//: C14:Wind.cpp
// Inheritance & upcasting
enum note { middleC, Csharp, Cflat }; // Etc.

class Instrument {
public:
  void play(note) const {}
};

// Wind objects are Instruments
// because they have the same interface:
class Wind : public Instrument {};

void tune(Instrument& i) {
  // ...
  i.play(middleC);
}

int main() {
  Wind flute;
  tune(flute); // Upcasting
} ///:~ 

What’s interesting in this example is the tune( ) function, which accepts an Instrument reference. However, in main( ) the tune( ) function is called by giving it a Wind object. Given that C++ is very particular about type checking, it seems strange that a function that accepts one type will readily accept another type, until you realize that a Wind object is also an Instrument object, and there’s no function that tune( ) could call for an Instrument that isn’t also in Wind. Inside tune( ), the code works for Instrument and anything derived from Instrument, and the act of converting a Wind object, reference, or pointer into an Instrument object, reference, or pointer is called upcasting.

Why “upcasting”?

The reason for the term is historical and is based on the way class inheritance diagrams have traditionally been drawn: with the root at the top of the page, growing downward. (Of course, you can draw your diagrams any way you find helpful.) The inheritance diagram for Wind.cpp is then:

Casting from derived to base moves up on the inheritance diagram, so it’s commonly referred to as upcasting. Upcasting is always safe because you’re going from a more specific type to a more general type – the only thing that can occur to the class interface is that it can lose member functions, not gain them. This is why the compiler allows upcasting without any explicit casts or other special notation.

Downcasting

You can also perform the reverse of upcasting, called downcasting, but this involves a dilemma that is the subject of Chapter 17.

Upcasting and the copy-constructor (not indexed)

If you allow the compiler to synthesize a copy-constructor for a derived class, it will automatically call the base-class copy-constructor, and then the copy-constructors for all the member objects (or perform a bitcopy on built-in types) so you’ll get the right behavior:

//: C14:Ccright.cpp
// Correctly synthesizing the CC
#include <iostream>
using namespace std;

class Parent {
  int i;
public:
  Parent(int ii) : i(ii) {
    cout << "Parent(int ii)\n";
  }
  Parent(const Parent& b) : i(b.i) {
    cout << "Parent(Parent&)\n";
  }
  Parent() :i(0) { cout << "Parent()\n"; }
  friend ostream&
    operator<<(ostream& os, const Parent& b) {
    return os << "Parent: " << b.i << endl;
  }
};

class Member {
  int i;
public:
  Member(int ii) : i(ii) {
    cout << "Member(int ii)\n";
  }
  Member(const Member& m) : i(m.i) {
    cout << "Member(Member&)\n";
  }
  friend ostream&
    operator<<(ostream& os, const Member& m) {
    return os << "Member: " << m.i << endl;
  }
};

class Child : public Parent {
  int i;
  Member m;
public:
  Child(int ii) : Parent(ii), i(ii), m(ii) {
    cout << "Child(int ii)\n";
  }
  friend ostream&
    operator<<(ostream& os, const Child& d){
    return os << (Parent&)d << d.m
              << "Child: " << d.i << endl;
  }
};

int main() {
  Child d(2);
  cout << "calling copy-constructor: " << endl;
  Child d2 = d; // Calls copy-constructor
  cout << "values in d2:\n" << d2;
} ///:~ 

The operator<< for Child is interesting because of the way that it calls the operator<< for the Parent part within it: by casting the Child object to a Parent& (if you cast to a Parent object instead of a reference you’ll end up creating a temporary):

return os << (Parent&)d << d.m

Since the compiler then sees it as a Parent, it calls the Parent version of operator<<.

You can see that Child has no explicitly-defined copy-constructor. The compiler then synthesizes the copy-constructor (since that is one of the four functions it will synthesize, along with the default constructor – if you don’t create any constructors – the operator= and the destructor) by calling the Parent copy-constructor and the Member copy-constructor. This is shown in the output

Parent(int ii)
Member(int ii)
Child(int ii)
calling copy-constructor:
Parent(Parent&)
Member(Member&)
values in d2:
Parent: 2
Member: 2
Child: 2 
However, if you try to write your own copy-constructor for Child and you make an innocent mistake and do it badly:

Child(const Child& d) : i(d.i), m(d.m) {}

The default constructor will be automatically called, since that’s what the compiler falls back on when it has no other choice of constructor to call (remember that some constructor must always be called for every object, regardless of whether it’s a subobject of another class). The output will then be:

Parent(int ii)
Member(int ii)
Child(int ii)
calling copy-constructor:
Parent()
Member(Member&)
values in d2:
Parent: 0
Member: 2
Child: 2 
This is probably not what you expect, since generally you’ll want the base-class portion to be copied from the existing object to the new object as part of copy-construction.

To repair the problem you must remember to properly call the base-class copy-constructor (as the compiler does) whenever you write your own copy-constructor. This can seem a little strange-looking at first but it’s another example of upcasting:

  Child(const Child& d)
    : Parent(d), i(d.i), m(d.m) {
    cout << "Child(Child&)\n";
  } 
The strange part is where the Parent copy-constructor is called: Parent(d). What does it mean to pass a Child object to a Parent constructor? Here’s the trick: Child is inherited from Parent, so a Child reference is a Parent reference. So the base-class copy-constructor upcasts a reference to Child to a reference to Parent and uses it to perform the copy-construction. When you write your own copy constructors you’ll generally want to do this.

Composition vs. inheritance (revisited)

One of the clearest ways to determine whether you should be using composition or inheritance is by asking whether you’ll ever need to upcast from your new class. Earlier in this chapter, the Stack class was specialized using inheritance. However, chances are the Stringlist objects will be used only as String containers, and never upcast, so a more appropriate alternative is composition:

//: C14:Inhstak2.cpp
//{L} ../C13/Stack11
// Composition vs. inheritance
#include <iostream>
#include <fstream>
#include <string>
#include "../require.h"
#include "../C13/Stack11.h"
using namespace std;

class StringList {
  Stack stack; // Embed instead of inherit
public:
  void push(string* str) {
    stack.push(str);
  }
  string* peek() const {
    return (string*)stack.peek();
  }
  string* pop() {
    return (string*)stack.pop();
  }
};

int main() {
  ifstream file("Inhstak2.cpp");
  assure(file, "Inhstak2.cpp");
  string line;
  StringList textlines;
  while(getline(file,line))
    textlines.push(new string(line));
  string* s;
  while((s = textlines.pop()) != 0) // No cast!
    cout << *s << endl;
} ///:~ 

The file is identical to Inhstack.cpp, except that a Stack object is embedded in Stringlist, and member functions are called for the embedded object. There’s still no time or space overhead because the subobject takes up the same amount of space, and all the additional type checking happens at compile time.

You can also use private inheritance to express “implemented in terms of.” The method you use to create the Stringlist class is not critical in this situation – they all solve the problem adequately. One place it becomes important, however, is when multiple inheritance might be warranted. In that case, if you can detect a class where composition can be used instead of inheritance, you may be able to eliminate the need for multiple inheritance.

Pointer & reference upcasting

In Wind.cpp, the upcasting occurs during the function call – a Wind object outside the function has its reference taken and becomes an Instrument reference inside the function. Upcasting can also occur during a simple assignment to a pointer or reference:

Wind w;
Instrument* ip = &w; // Upcast
Instrument& ir = w; // Upcast 
Like the function call, neither of these cases require an explicit cast.

A crisis

Of course, any upcast loses type information about an object. If you say

Wind w;
Instrument* ip = &w; 
the compiler can deal with ip only as an Instrument pointer and nothing else. That is, it cannot know that ip actually happens to point to a Wind object. So when you call the play( ) member function by saying

ip->play(middleC);

the compiler can know only that it’s calling play( ) for an Instrument pointer, and call the base-class version of Instrument::play( ) instead of what it should do, which is call Wind::play( ). Thus you won’t get the correct behavior.

This is a significant problem; it is solved in the next chapter by introducing the third cornerstone of object-oriented programming: polymorphism (implemented in C++ with virtual functions).

Contents | Prev | Next


Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru