xSKILLby Infirals
Register

C++ for Beginners

0 of 24 notes read 0%

Reading progress is saved to your INFIRALS account. Sign in to keep this note marked as read.

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.

cpp
#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.

cpp
#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.

cpp
#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.

cpp
#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.

cpp
#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

  1. Add a Cat class that overrides speak and pass it to announce.
  2. Remove virtual from the base function and compare the output.
  3. Add override to a function with the wrong parameter type and read the compiler error.
  4. Pass a derived object by value and by reference to the same function.
  5. Add a virtual destructor to a base class and print from both destructors when deleting through a base pointer.

Interview questions

3 questions from this note. Open one to see the answer; no account needed.

  • 01What does the `override` keyword check?

    It tells the compiler that the function must override a virtual function from a base class. If the signature does not match any base virtual function, compilation fails, which catches misspelled names and wrong parameter types.

  • 02Why does a polymorphic base class need a virtual destructor?

    Deleting a derived object through a base pointer needs a virtual destructor in the base. Without one, the destructor call uses the base type, so only the base destructor runs and the derived part is not destroyed. Its resources can leak.

  • 03What happens when a derived object is passed by value to a function taking the base type?

    The base copy constructor runs and copies only the base part. The result is a base object, so virtual calls use the base version. This is object slicing.