Virtual functions and overriding
Inheritance, operators, and templates13 min readLesson 2 of 5
2.1 One call, several versions
A call through a base reference or pointer uses the static type of the expression unless the function is virtual. If Animal has a plain speak and Dog defines its own, Animal& animal = dog; animal.speak(); calls the animal version. The compiler has already chosen that function from the reference type.
Marking the base function virtual changes the rule. The call now looks at the object's dynamic type and runs the most derived override. A function that takes const Animal& can receive a Dog, a Cat, or any other derived object, and each one runs its own version.
#include <iostream>
class Animal {
public:
virtual void speak() const { std::cout << "...\n"; }
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void speak() const override { std::cout << "Woof\n"; }
};
void announce(const Animal& animal) { animal.speak(); }
int main() {
Dog dog;
announce(dog);
return 0;
}Output
Woof
2.1.1 The override keyword
override tells the compiler that a function must replace a base virtual function. If the signature does not match, the compiler reports an error instead of creating a different function. A missing const, a different parameter type, or a misspelled name becomes visible at the definition.
The base function needs virtual. The derived function does not need to repeat the keyword, and override is the clearer marker. A class can mark a function final to stop further overriding. These markers cost nothing at runtime and catch mistakes that otherwise survive until a call behaves unexpectedly.
#include <iostream>
class Shape {
public:
virtual double area() const { return 0.0; }
virtual ~Shape() = default;
};
class Square : public Shape {
public:
explicit Square(double side) : side_(side) {}
double area() const override { return side_ * side_; }
private:
double side_;
};
void print_area(const Shape& shape) { std::cout << "Area: " << shape.area() << "\n"; }
int main() {
Square square(4.0);
print_area(square);
return 0;
}Output
Area: 16
2.1.2 How the call is chosen
Without virtual, the compiler chooses a function from the static type of the expression. With virtual, the call goes through the object's dynamic type, so a base reference calls the derived override.
Most compilers implement this with a table of function pointers per class and a hidden pointer in each polymorphic object. The details belong to the compiler and platform, and the cost is one indirect call. A virtual call cannot be inlined as reliably as a direct one, so use virtual functions where runtime substitution is part of the design.
#include <iostream>
class Base {
public:
virtual ~Base() { std::cout << "Base gone\n"; }
};
class Derived : public Base {
public:
~Derived() override { std::cout << "Derived gone\n"; }
};
int main() {
Base* item = new Derived();
delete item;
return 0;
}Output
Derived gone Base gone
2.1.3 Object slicing
A derived object contains a base subobject. Copying or assigning that derived object into a base object copies only the base part, and the derived members disappear. The result is a complete base object, so virtual calls use the base version.
Slicing happens when a function takes its parameter by value, when a base object is initialized from a derived one, and when a derived object is stored in a std::vector<Base>. Pass polymorphic objects by reference or pointer. A container that must hold several derived types stores pointers or smart pointers.
#include <iostream>
class Animal {
public:
virtual void speak() const { std::cout << "...\n"; }
};
class Dog : public Animal {
public:
void speak() const override { std::cout << "Woof\n"; }
};
void announce(Animal animal) { animal.speak(); }
void announce_ref(const Animal& animal) { animal.speak(); }
int main() {
Dog dog;
announce(dog);
announce_ref(dog);
return 0;
}Output
... Woof
2.1.4 Defaults and virtual calls in constructors
A default argument on a virtual function is chosen from the static type, while the function body is chosen from the dynamic type. Those two choices can disagree, so avoid defaults on virtual functions and pass the value explicitly.
During base class construction, the derived part does not exist yet. A virtual call in a base constructor therefore uses the base version, and the same rule applies in a base destructor. Calling virtual functions from constructors and destructors is legal, and it rarely does what a beginner expects.
#include <iostream>
class Base {
public:
Base() { describe(); }
virtual void describe() const { std::cout << "Base\n"; }
};
class Derived : public Base {
public:
void describe() const override { std::cout << "Derived\n"; }
};
int main() {
Derived item;
return 0;
}Output
Base
2.1.5 When virtual functions fit
Use virtual functions when a program treats several types through one interface and the operation depends on the concrete type. A drawing program can hold shapes and ask each one for its area. A payment system can process different payment methods through one base class.
Virtual dispatch adds a layer of indirection and a base class that every implementation depends on. If a program never uses a base reference or pointer, a plain function or a template may be simpler. Mark a class final when it should not be derived from, and mark overrides override so mistakes surface at compile time.
2.1.6 Practice
- Add a
Catclass that overridesspeakand pass it toannounce. - Remove
virtualfrom the base function and compare the output. - Add
overrideto a function with the wrong parameter type and read the compiler error. - Pass a derived object by value and by reference to the same function.
- Add a virtual destructor to a base class and print from both destructors when deleting through a base pointer.
