Summary

Today's lab session covers class inheritance and the use of abstract classes. Of special importance are the details concerning creation, destruction and copying of objects within class hierarchies. Along the way you will learn a small & handy C++11 feature. The main exercise builds upon the function-plotter exercise you did last week.

Class inheritance

Designing class hierarchies by deriving concrete classes from more abstract ones is one of the most essential features of object-oriented programming languages; also C++. A point worth noting is that the concept of deriving classes predates the introduction of C++ by roughly 15 years. By no means is C++ a purely object-oriented language, nor is it the most elegant implementation of the principles. Being a very popular general purpose language, C++ has many of the features that allow for an object-oriented approach of designs. Keep in mind though, that there are many other languages that are "more object-oriented" than C++. Do some reading if you're interested in some more details. Since the lab sessions are all about practical examples, we'll jump right into the syntax of deriving a class Derived from another class Base:

class Base {
    public:
        void foo();
};

class Derived : public Base {
    public:
        void bar();
};

In this simple case, the UML diagram (generated from the above code snippet with Doxygen by the way) looks like: base_derived Objects of the Derived class have two members: foo (derived from Base) and bar (defined in Derived). Therefore, the following is valid:

Derived d;
d.foo();
d.bar();

More generally: a member of a derived class can use the public and protected members of its base class as if they were declared in the derived class.

You can say that a derived class is "larger" than its base class in the sense that it holds more data and provides more functions.

Note the public inheritance. This means that all access modifiers (public, private and protected) of the Base class members stay the same in Derived. This is the most common scenario we'll encounter. Two other possibilities are:

  • protected: public members of Base become protected in Derived
  • private: all members from Base become private in Derived

Constructors & destructors

If Base has constructors, they must be invoked in Derived. This invocation can be implicit (i.e., automatic) in the case of a default constructor. On the other hand, if Base's constructor needs arguments, it must be called explicitly. This can be summarized as: constructors are never inherited. Also, you can't directly initialize members of Base in the constructor of Derived; even if they're not private (assignment works though).

Base.h:

class Base {
    public:
        Base(int a);
};

Base.cpp:

Base::Base(int a) {
    ...
}

Derived.h:

class Derived : public Base {
    public:
        Derived(int a, int b);
};

Derived.cpp:

Derived::Derived(int a, int b)
    : Base(a) { // <- EXPLICIT call to Base's non-default constructor
    ...
}

The order in which objects are created can be summarized as:

  • Objects are constructed bottom-up: base class, members, derived class
  • Objects are destroyed top down: derived class, members, base class
  • Members & base classes are constructed in order of declaration
  • Members & base classes are destroyed in reverse order of declaration

These rules are really very important. One part of the final exam is performing a trace where you explicitly write out the order of construction & destruction operations on objects. Make sure you know it all!

C++11 additions

The above is not 100% true anymore. Please learn about constructor inheritance here and here to see what's been added to C++11 to ease object creation for inherited classes.

Suppose the following scenario:

Base.h:

class Base {
    public:
        Base(int a);
};

Base.cpp:

Base::Base(int a) {
    cout << "Base constructor, a = " << a << endl;
}

Derived.h:

class Derived : public Base {
};

main.cpp:

// -- SNIP --

Derived d(1);

// -- SNAP --

My g++ fails with error: no matching function for call to 'Derived::Derived(int)'. C++11 provides a way to fix this. Read the above Wikipedia article and make the example work! In both articles, another interesting C++11 feature is discussed: constructor delegation. Which part of the following snippet relate to constructor inheritance and which part to constructor delegation?

class Base {
public:
    Base(int n) : number(n) {};
private:
    int number;
};

class Derived: public Base {
public:
    using Base::Base;
    Derived() : Derived(5) {};
};

Making copies of objects

Suppose you're doing this:

Derived d;
Base b = d;

or an assignment like:

Derived d;
Base b;
b = d;

The result is that b only knows about Base properties of d. All the rest is not copied. This is called slicing. There's an interesting consequence of how constructors are handled in class hierarchies when you're making copies of objects. Consider:

Base.h:

class Base {
    public:
        Base();
        Base(const Base&);
};

Base.cpp:

Base::Base() {
    cout << "Base::Base()" << endl;
}

Base::Base(const Base&) {
    cout << "Base::Base(const Base&)" << endl;
}

Derived.h:

class Derived : public Base {
    public:
        Derived();
        Derived(const Derived&);
};

Derived.cpp:

Derived::Derived() {
    cout << "Derived::Derived()" << endl;
}

Derived::Derived(const Derived&) {
    cout << "Derived::Derived(const Derived&)" << endl;
}

What's the output when I do:

Derived d;
Derived d_copy(d);

Do you think what you see is the expected behaviour? If not, what's wrong, and how should you fix it? Check if the assignment operator operator= is similar in how the above situation is handled.

Pointers / references

Contrary to the above, when passing around pointers or references to objects, no copies are involved, nor is there any slicing (i.e., no information is lost). For example:

Derived* d_ptr = new Derived;
Base* b_ptr = d_ptr;

Now b_ptr pretends to be a pointer to Base but is actually pointing to a Derived object. This brings us to the important subject of virtual functions.

Virtual functions

Referring to Derived objects through a pointer / reference to Base allows using polymorphic method calls. Try this example:

Base.h:

class Base {
    public:
        virtual void print_info();
};

Base.cpp:

void Base::print_info() {
    cout << "Base::print_info()" << endl;
}

Derived.h:

class Derived : public Base {
    public:
        virtual void print_info(); // NOTE: 'virtual' is optional here, 'override' might be better.
};

Derived.cpp:

void Derived::print_info() {
    cout << "Derived::print_info()" << endl;
}

Note that the keyword virtual only appears in the class definition (.h file); NOT in the definition of its methods (.cpp file)!

Now, different print_info methods will be called depending on object's type:

Base* b_ptr = new Base;
Derived* d_ptr = new Derived;
Base* b_ptr2 = new Derived;
Base& b_ref = *d_ptr;

b_ptr->print_info();    // Base::print_info()
d_ptr->print_info();    // Derived::print_info()
b_ptr2->print_info();   // Derived::print_info() !!!
b_ref.print_info();     // Derived::print_info() !!!

In all cases the object's dynamic type determines which method is called. The dynamic type is the type of the object pointed to at runtime (remember that a pointer to a Base object can actually point to a Derived object).

The C++ FAQ lite has a whole section on virtual functions. Especially the part on how virtual functions work internally is interesting if you want a deeper knowledge of C++!

This is called polymorphism. In C++ it only works with pointers and references to objects. Try removing the virtual keyword from the above example. What happens?

Abstract classes

A slight downside to virtual functions as defined above is that you are required to implement all methods, even if they really don't do anything useful yet, as might be the case in the Base class. More often than not a base class specifies how derived classes should look like (i.e., the interface). The consequence is that many of the base class methods end up having an empty body.

class TheAnswer {
    public:
        virtual int get() {
            // The base class represents a concept that is too general to
            // decide what exectly should be returned. However, the get()
            // method needs to be implemented or the code won't compile.
            // If you turn on all warnings in gcc with -Wall, you'll even
            // get:
            //  warning: no return statement in function returning non-void
            // but you don't know what to return!
            // Surely there must be a better way to handle this...
        }
};

class TheAnswerToLifeTheUniverseAndEverything : public TheAnswer {
    public:
        virtual int get() {
            return 42;
        }
};

The solution to the above problem is declaring TheAnswer::get() to be a pure virtual function. A pure virtual function has no definition, only a zero-initializer:

class TheAnswer {
    public:
        // We don't need to make up answers just to please the compiler!
        virtual int get() = 0;
}

As you might have guessed, it now doesn't make any sense to create objects of the type TheAnswer since they don't know how to behave if you call their get() method. Any attempt to compile this:

TheAnswer a;

Indeed gives:

error: cannot declare variable 'a' to be of abstract type 'TheAnswer'
note:  because the following virtual functions are pure within 'TheAnswer':
note:   virtual int TheAnswer::get() = 0;

As GCC's error message hints, we'll call TheAnswer an abstract base class. Read C++ FAQ's section on abstract base classes now.

Notes

  • A pure virtual function that is not defined in a derived class remains a pure virtual function. Therefore, the derived class remains an abstract class.
  • This way, layers of abstraction can be stacked together.
  • Abstract classes represent abstract concepts and work as interfaces for its derived classes to enforce a certain structure.
  • A pure virtual function may be defined in the base class if its functionality is used frequently in most of the derived classes. The zero-initializer still ensures the abstract behaviour of the base class.

Run-time type information

(often abbreviated as RTTI)

In the context of class hierarchies three different types of casts can be performed between pointers (or references) to objects of different types:

  • upcast: from Derived* to Base*
  • downcast: from Base* to Derived*
  • crosscast: from a Base1* to a Base2 (only for multiple inheritance)

Upcast

"Whatever points to an object of the Derived class, must also point to a Base":

Derived* d_ptr = new Derived;
Base* b_ptr = d_ptr;

Downcast

"If you've got a pointer to a Base object, you can't assume it's pointing to a Derived object". Unless you know for sure, in which case you can use a dynamic_cast:

Base* b_ptr = new Derived;
Derived* d_ptr = dynamic_cast<Derived*>(b_ptr);

Note that the use of dynamic_cast is often frowned upon, as this usually means you applied polymorphism incorrectly. Look at the concept of duck typing and the Liskov substitution principle for when to correctly apply inheritance. An alternative to downcasting can be a double dispatch, potentially in combination with the visitor pattern. Also, have a look at some discussions about the use of dynamic_cast, for instance here or here.

Now, try to remove the virtual function from the Base class. What happens?

Crosscast

Google exercise: look it up!

Notes

RTTI deals with the case in which the correctness of the type conversion cannot be determined by the compiler at compile-time and must be postponed until run-time. The primary purpose of the dynamic_cast operator is to perform type-safe downcasts. If the conversion fails, a null pointer is returned. In the case of references, a bad_cast exception is thrown.

You can use it for checking the exact type of a polymorphic object:

// Both Derived1 and Derived2 are subclasses of Base
void f(Base* b_ptr) {
    if (Derived1* d1_ptr = dynamic_cast<Derived1*>(b_ptr)) {
        d1_ptr->derived1Method();
    } else if (Derived2* d2_ptr = dynamic_cast<Derived2*>(b_ptr)) {
        d2_ptr->derived2Method();
    } else {
        b_ptr->baseMethod();
    }
}

Exercises

Plotting polynomials, part 2

Go to the assignment: https://classroom.github.com/a/gSBbgdEq

Virtual destructors

Go to the assignment: https://classroom.github.com/a/Hfth3gX_