DEV Community

John Calabrese
John Calabrese

Posted on

virtual and override in C++

Here's an example I wish I'd seen earlier when learning about inheritance in C++.

Let's start with a simple base class.

class Base {
public:
    void method() {
        speak("base method\n");
    }
};
Enter fullscreen mode Exit fullscreen mode

Here speak is just a wrapper around cout and endl.

void speak(const string out) {
    cout << out << endl;
}
Enter fullscreen mode Exit fullscreen mode

Let's now inherit from Base and re-use the same method name.

class Derived : public Base {
public:
    void method() {
        speak("derived method\n");
    }
};
Enter fullscreen mode Exit fullscreen mode

So: base classes print "base", derived classes print "derived". However, what happens if we mix types? Concretely, what would the output of the following program be?

#include <iostream>
using std::cout;
using std::endl;
using std::string;

// convenience function to print
void speak(const string out) {
    cout << out << endl;
}

class Base {
public:
    void method() {
        speak("base method\n");
    }
};

class Derived : public Base {
public:
    void method() {
        speak("derived method\n");
    }
};


int main() {
    speak("ex1: base class");
    Base base = Base();
    base.method();

    speak("ex2: derived class");
    Derived derived = Derived();
    derived.method();

    speak("ex3: base class ref");
    Base& derived_ref = derived;
    derived_ref.method();

    // Derived& base_ref_error = base;
    // error: non-const lvalue reference to type 'Derived' cannot bind to a 
    // value of unrelated type 'Base'
}
Enter fullscreen mode Exit fullscreen mode

Examples 1 and 2 are straightforward. What about example 3? Intuitively, we expect derived_ref.method() to print out "derived". It is a reference to a Derived object. However, this is the output:

ex1: base class
base method

ex2: derived class
derived method

ex3: base class ref
base method
Enter fullscreen mode Exit fullscreen mode

Shock! The reason is that, in example 3, derived_ref is of type Base&. The name is misleading: it's really a base_ref :). When it looks for method it looks it up in the Base class.

When dealing with inheritance this situation is more common than one would like, especially when dealing with abstract classes / interfaces.

As an aside: the last example Derived& base_ref_error = base; would not compile if uncommented.

runtime polymorphism

So, how do we fix example 3 above? The solution is to mark the base class method as virtual.

#include <iostream>
using std::cout;
using std::endl;
using std::string;

// convenience function to print
void speak(const string out) {
    cout << out << endl;
}

class Base {
public:
    virtual void method() {
        speak("base method\n");
    }
};

class Derived : public Base {
public:
    void method() override {
        speak("derived method\n");
    }
};


int main() {
    speak("ex1: base class");
    Base base = Base();
    base.method();

    speak("ex2: derived class");
    Derived derived = Derived();
    derived.method();

    speak("ex3: base class ref");
    Base& derived_ref = derived;
    derived_ref.method();
}
Enter fullscreen mode Exit fullscreen mode

This time the output is:

ex1: base class
base method

ex2: derived class
derived method

ex3: base class ref
derived method
Enter fullscreen mode Exit fullscreen mode

Great! This makes more sense. The variable derived_ref is, after all, built from derived, which is of type Derived.

Note: the override keyword in the Derived class is unnecessary. However, it's useful to have it there to help the compiler find bugs.

abstract classes

A typical scenario where the virtual stuff comes up is with abstract classes. Here is an example:

class AbstractBase {
public:
    virtual void method() = 0;
    virtual ~AbstractBase() = default;
};
Enter fullscreen mode Exit fullscreen mode

We have modified base to have method be unimplemented. The =0 means we require any derived class to implement that method.

We also mark the destructor as virtual, as subtle bugs can arise if we don't. See below.

#include <iostream>
using std::cout;
using std::endl;
using std::string;

// convenience function to print
void speak(const string out) {
    cout << out << endl;
}

class AbstractBase {
public:
    virtual void method() = 0;
    virtual ~AbstractBase() = default;
};

class Derived : public AbstractBase {
public:
    void method() override {
        speak("derived method\n");
    }
};


int main() {
    // AbstractBase abstractbase = AbstractBase();
    // error: allocating an object of abstract class type 'AbstractBase''Base'

    speak("ex2: derived class");
    Derived derived = Derived();
    derived.method();

    speak("ex3: base class ref");
    AbstractBase& derived_ref = derived;
    derived_ref.method();
}
Enter fullscreen mode Exit fullscreen mode

The output is

ex2: derived class
derived method

ex3: base class ref
derived method
Enter fullscreen mode Exit fullscreen mode

as expected.

As an aside: the first example AbstractBase abstractbase = AbstractBase(); does not compile, as AbstractBase is, indeed, abstract.

destructors

Let's clarify what happens if we don't have a destructor marked as virtual. Here is a first example.

#include <iostream>
using std::cout;
using std::endl;
using std::string;

// convenience function to print
void speak(const string out) {
    cout << out << endl;
}

class Base {
public:
    ~Base() {
        speak("base destruct");
    }
};

class Derived : public Base {
public:
    ~Derived() {
        speak("derived destruct");
    }
};

int main() {
    Derived der; 
}
Enter fullscreen mode Exit fullscreen mode

If we run this, the output is

derived destruct
base destruct
Enter fullscreen mode Exit fullscreen mode

as the destructors for der are invoked in order. However, tweaking this slightly, we get some unexpected behavior (I got this example from "C++ Crash Course" by Josh Lospinoso).

#include <iostream>
using std::cout;
using std::endl;
using std::string;

// convenience function to print
void speak(const string out) {
    cout << out << endl;
}

class Base {
public:
    ~Base() {
        speak("base destruct");
    }
};

class Derived : public Base {
public:
    ~Derived() {
        speak("derived destruct");
    }
};

int main() {
    Base* x{new Derived()};
    delete x;
}
Enter fullscreen mode Exit fullscreen mode

The output is

base destruct
Enter fullscreen mode Exit fullscreen mode

so ~Derived is never invoked! This could lead to serious issues. The simple fix is to have the destructor be virtual.

#include <iostream>
using std::cout;
using std::endl;
using std::string;

// convenience function to print
void speak(const string out) {
    cout << out << endl;
}

class Base {
public:
    virtual ~Base() {
        speak("base destruct");
    }
};

class Derived : public Base {
public:
    ~Derived() {
        speak("derived destruct");
    }
};

int main() {
    Base* x{new Derived()};
    delete x;
}
Enter fullscreen mode Exit fullscreen mode

so now the output is

derived destruct
base destruct
Enter fullscreen mode Exit fullscreen mode

as expected.

Top comments (0)