Free Handbook · Every example compiled & verified

Inheritance & Polymorphism

Public inheritance, protected members, virtual functions and override, abstract classes, virtual destructors, object slicing, final, dynamic_cast, and when to prefer composition.

0 / 145 lessons🔥 0 day streak
ShareXLinkedIn

Module 08 · what you'll be able to do

  • Derive a class from a base, call the base constructor, and predict construction and destruction order
  • Use virtual and override so a call through a base pointer or reference runs the derived version
  • Write an abstract interface with pure virtual functions and give every polymorphic base a virtual destructor
  • Spot object slicing and avoid it by holding polymorphic objects through references or smart pointers
  • Use final and dynamic_cast appropriately, and choose composition over inheritance when "has-a" fits better
01

Public inheritance

Inheritance lets a new class (the derived class) start from an existing one (the base class): class Manager : public Employee. A Manager has every member an Employee has, plus its own. Public inheritance models an is-a relationship: a manager is an employee, so anywhere an Employee& is expected, a Manager can be passed.

A derived object contains a complete base object inside it. So construction runs base first, then the derived members, then the derived constructor body; destruction runs in exactly the reverse order. The derived constructor chooses which base constructor to call in its member initializer list (Module 07).

C++main.cpp
#include <iostream>
#include <string>

class Employee {
public:
    Employee(std::string name, int salary) : name_(std::move(name)), salary_(salary) {
        std::cout << "  Employee ctor\n";
    }
    ~Employee() { std::cout << "  Employee dtor\n"; }
    const std::string& name() const { return name_; }
    int salary() const { return salary_; }
private:
    std::string name_;
    int salary_;
};

class Manager : public Employee {
public:
    Manager(std::string name, int salary, int reports)
        : Employee(std::move(name), salary), reports_(reports) {   // base first
        std::cout << "  Manager ctor\n";
    }
    ~Manager() { std::cout << "  Manager dtor\n"; }
    int reports() const { return reports_; }
private:
    int reports_;
};

void printBadge(const Employee& e) {            // accepts any Employee, including a Manager
    std::cout << "badge: " << e.name() << '\n';
}

int main() {
    std::cout << "building:\n";
    Manager m("Meera", 150000, 6);
    std::cout << m.name() << " earns " << m.salary() << " and has " << m.reports() << " reports\n";
    printBadge(m);
    std::cout << "leaving main:\n";
}
Outputcompiled & run with real C++
building:
  Employee ctor
  Manager ctor
Meera earns 150000 and has 6 reports
badge: Meera
leaving main:
  Manager dtor
  Employee dtor
Your turn

Add a third level, class Director : public Manager, with its own constructor and destructor messages. Predict the six lines before you run it.

public, protected and private inheritance
The keyword after the colon controls how the base's public members look from outside. public inheritance keeps them public and is what you want for is-a relationships almost every time. private inheritance (the default for class, which is why you should always write public) hides them and means "implemented in terms of"; composition usually says that more clearly.
02

protected members

A derived class does not get access to its base's private members. They exist inside the derived object, but only the base's own functions can touch them. That is deliberate: the base keeps control of its invariant. protected is the middle ground: accessible to the class and to classes derived from it, but not to outside code.

AccessThe class itselfDerived classesEveryone else
publicyesyesyes
protectedyesyesno
privateyesnono
Error you will hit

'balance_' is a private member of 'Account' (from a derived class)

C++
#include <string>

class Account {
public:
    explicit Account(int b) : balance_(b) {}
private:
    int balance_;
};

class Savings : public Account {
public:
    using Account::Account;
    void addInterest() { balance_ += balance_ / 20; }
};

int main() {
    Savings s(1000);
    s.addInterest();
}
main.cpp:13:26: error: 'balance_' is a private member of 'Account'
   13 |     void addInterest() { balance_ += balance_ / 20; }
      |                          ^
main.cpp:7:9: note: declared private here
    7 |     int balance_;
      |         ^
Why the compiler said that

Savings inherits balance_ (every Savings object contains one), but private means only Account's own member functions may use it. Clang reports the same error again for the second use on line 13. (using Account::Account; is unrelated: it inherits the base constructors.)

The fix

Prefer giving the base a protected (or public) member function that derived classes call, so the base still validates every change. Making the data member itself protected also compiles, but then every derived class can break the invariant.

C++
class Account {
public:
    explicit Account(int b) : balance_(b) {}
    int balance() const { return balance_; }
protected:
    void credit(int amount) { if (amount > 0) balance_ += amount; }
private:
    int balance_;
};

class Savings : public Account {
public:
    using Account::Account;
    void addInterest() { credit(balance() / 20); }
};
C++main.cpp
#include <iostream>

class Account {
public:
    explicit Account(int b) : balance_(b) {}
    int balance() const { return balance_; }
protected:
    void credit(int amount) {             // derived classes may call this...
        if (amount > 0) balance_ += amount;
    }
private:
    int balance_;                         // ...but only Account touches this
};

class Savings : public Account {
public:
    Savings(int b, int ratePercent) : Account(b), rate_(ratePercent) {}
    void addInterest() { credit(balance() * rate_ / 100); }
private:
    int rate_;
};

int main() {
    Savings s(1000, 5);
    s.addInterest();
    s.addInterest();
    std::cout << s.balance() << '\n';
    // s.credit(1'000'000);               // error: credit is protected
}
Outputcompiled & run with real C++
1102
03

Virtual functions and override

Polymorphism means one call site, many behaviours: a function that takes a Shape& calls area(), and a circle computes a circle's area while a square computes a square's. That needs dynamic dispatch: choosing the function by the object's real (dynamic) type at runtime, not the declared (static) type of the pointer or reference.

In C++ you opt in per function with virtual in the base class. A derived class then overrides it with a function of exactly the same signature, and should say so with override, which makes the compiler check that it really does override something. Without virtual, the call is bound at compile time by the static type, and the base version runs. Dispatch only happens through a pointer or reference; calling on an object by value always uses its own type.

C++main.cpp
#include <iostream>
#include <memory>
#include <string>
#include <vector>

class Notifier {
public:
    virtual ~Notifier() = default;
    virtual std::string send(const std::string& msg) const {        // virtual: dispatched
        return "log: " + msg;
    }
    std::string channel() const { return "generic"; }              // not virtual
};

class EmailNotifier : public Notifier {
public:
    std::string send(const std::string& msg) const override { return "email: " + msg; }
    std::string channel() const { return "email"; }                // hides, does not override
};

class SmsNotifier : public Notifier {
public:
    std::string send(const std::string& msg) const override { return "sms: " + msg.substr(0, 11); }
};

void alert(const Notifier& n, const std::string& msg) {
    std::cout << n.send(msg) << "  [" << n.channel() << "]\n";
}

int main() {
    std::vector<std::unique_ptr<Notifier>> targets;
    targets.push_back(std::make_unique<Notifier>());
    targets.push_back(std::make_unique<EmailNotifier>());
    targets.push_back(std::make_unique<SmsNotifier>());
    for (const auto& t : targets) alert(*t, "disk almost full");
}
Outputcompiled & run with real C++
log: disk almost full  [generic]
email: disk almost full  [generic]
sms: disk almost  [generic]

send is virtual, so each object's own version runs. channel is not, so a call through Notifier& always runs Notifier::channel, even for an EmailNotifier.

VisualizeDynamic dispatch through a base referenceStep 1 / 4
void alert(const Notifier& n, const std::string& msg) {
std::cout << n.send(msg) << " [" << n.channel() << "]\n";
}
int main() {
EmailNotifier e;
alert(e, "disk almost full");
}
Line 5

An EmailNotifier is created. Because its class has virtual functions, the object carries a hidden pointer to its class's table of virtual functions (the vtable).

Variables now
eEmailNotifier (vptr -> EmailNotifier vtable)
All 4 steps as a table
StepLineWhat happenedVariables now
15An EmailNotifier is created. Because its class has virtual functions, the object carries a hidden pointer to its class's table of virtual functions (the vtable).e = EmailNotifier (vptr -> EmailNotifier vtable)
26e binds to const Notifier& n. Nothing is copied; n refers to the whole EmailNotifier.n = refers to e static type = Notifier dynamic type = EmailNotifier
32send is virtual: the program looks it up through the object's vtable at runtime and finds EmailNotifier::send.n = refers to e called = EmailNotifier::send
42channel is not virtual: the compiler already chose Notifier::channel from the static type Notifier. No lookup happens.n = refers to e called = Notifier::channel
Error you will hit

non-virtual member function marked 'override' hides virtual member function

C++
#include <iostream>

class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const { return 0; }
};

class Square : public Shape {
public:
    explicit Square(double s) : side_(s) {}
    double area() override { return side_ * side_; }
private:
    double side_;
};

int main() {
    Square sq(3);
    std::cout << sq.area() << '\n';
}
main.cpp:12:19: error: non-virtual member function marked 'override' hides virtual member function
   12 |     double area() override { return side_ * side_; }
      |                   ^
main.cpp:6:20: note: hidden overloaded virtual function 'Shape::area' declared here: different qualifiers ('const' vs unqualified)
    6 |     virtual double area() const { return 0; }
      |                    ^
Why the compiler said that

The base function is area() const; the derived one is area() without const. To the compiler those are different signatures, so this is a brand-new function, not an override, and calls through a Shape& would still return 0. override turned that silent bug into a compile error, and the note even names the difference.

The fix

Match the signature exactly, including const. Always write override on overriding functions so mismatches are caught.

C++
double area() const override { return side_ * side_; }
04

Pure virtual functions and abstract classes

Sometimes the base class has no sensible implementation: what is the area of a generic "shape"? Declare the function pure virtual with = 0. A class with at least one pure virtual function is abstract: you cannot create objects of it, only of derived classes that override every pure virtual function. A class made only of pure virtual functions (plus a virtual destructor) is C++'s version of an interface, like an interface in Java or C#.

C++main.cpp
#include <iostream>
#include <memory>
#include <numbers>
#include <vector>

class Shape {                                     // abstract: an interface
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;              // pure virtual
    virtual const char* name() const = 0;
};

class Circle : public Shape {
public:
    explicit Circle(double r) : r_(r) {}
    double area() const override { return std::numbers::pi * r_ * r_; }
    const char* name() const override { return "circle"; }
private:
    double r_;
};

class Rect : public Shape {
public:
    Rect(double w, double h) : w_(w), h_(h) {}
    double area() const override { return w_ * h_; }
    const char* name() const override { return "rect"; }
private:
    double w_, h_;
};

double totalArea(const std::vector<std::unique_ptr<Shape>>& shapes) {
    double sum = 0;
    for (const auto& s : shapes) sum += s->area();   // works for any future Shape too
    return sum;
}

int main() {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(1.0));
    shapes.push_back(std::make_unique<Rect>(2.0, 3.5));
    for (const auto& s : shapes) std::cout << s->name() << ": " << s->area() << '\n';
    std::cout << "total: " << totalArea(shapes) << '\n';
}
Outputcompiled & run with real C++
circle: 3.14159
rect: 7
total: 10.1416
Your turn

Add a Triangle with base and height. totalArea needs no change at all: that is the point of programming against an interface.

Error you will hit

variable type 'Shape' is an abstract class

C++
class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
};

int main() {
    Shape s;
}
main.cpp:8:11: error: variable type 'Shape' is an abstract class
    8 |     Shape s;
      |           ^
main.cpp:4:20: note: unimplemented pure virtual method 'area' in 'Shape'
    4 |     virtual double area() const = 0;
      |                    ^
Why the compiler said that

A Shape object would have no area() to call. The same error appears for a derived class that forgot to override one of the pure virtual functions: for a Circle that implements area but not perimeter, clang says variable type 'Circle' is an abstract class with the note unimplemented pure virtual method 'perimeter' in 'Circle'.

The fix

Create a concrete derived class, and hold it through a Shape&, Shape* or std::unique_ptr<Shape>. If the error is about your derived class, implement the function the note names.

C++
std::unique_ptr<Shape> s = std::make_unique<Circle>(2.0);
05

Virtual destructors

When you delete an object through a base-class pointer (which is exactly what a std::unique_ptr<Base> does), the destructor call is dispatched like any other member function. If the base destructor is not virtual, only the base destructor runs, and the derived part is never cleaned up. Formally it is undefined behaviour. The rule is simple: any class meant to be used polymorphically needs a virtual destructor, usually just virtual ~Base() = default;.

C++main.cpp
#include <iostream>
#include <memory>
#include <string>

class Logger {
public:
    virtual ~Logger() { std::cout << "~Logger\n"; }                 // virtual
    virtual void log(const std::string& msg) { std::cout << msg << '\n'; }
};

class FileLogger : public Logger {
public:
    ~FileLogger() override { std::cout << "~FileLogger: flush and close file\n"; }
    void log(const std::string& msg) override { std::cout << "[file] " << msg << '\n'; }
};

int main() {
    std::unique_ptr<Logger> logger = std::make_unique<FileLogger>();
    logger->log("started");
}   // unique_ptr deletes through Logger*: both destructors run, derived first
Outputcompiled & run with real C++
[file] started
~FileLogger: flush and close file
~Logger
Error you will hit

delete called on non-final class that has virtual functions but non-virtual destructor

C++
#include <iostream>
#include <string>

class Logger {
public:
    ~Logger() { std::cout << "~Logger\n"; }
    virtual void log(const std::string& msg) { std::cout << msg << '\n'; }
};

class FileLogger : public Logger {
public:
    ~FileLogger() { std::cout << "~FileLogger: flush and close file\n"; }
    void log(const std::string& msg) override { std::cout << "[file] " << msg << '\n'; }
};

int main() {
    Logger* logger = new FileLogger();
    logger->log("started");
    delete logger;
}
$ c++ -std=c++20 -Wall main.cpp && ./a.out
main.cpp:19:5: warning: delete called on non-final 'Logger' that has virtual functions but non-virtual destructor [-Wdelete-non-abstract-non-virtual-dtor]
   19 |     delete logger;
      |     ^
1 warning generated.
[file] started
~Logger
Why the compiler said that

The warning only appears with -Wall; a plain compile is silent. The run shows the damage: log was dispatched to FileLogger, but the destructor was not, so ~FileLogger never ran and the "file" was never flushed or closed. Any members FileLogger had (strings, vectors, handles) were never destroyed either. The same happens with std::unique_ptr<Logger>, and then there is not even a delete line to warn about.

The fix

Make the base destructor virtual. Then every destructor in the chain runs, most-derived first.

C++
class Logger {
public:
    virtual ~Logger() { std::cout << "~Logger\n"; }
    virtual void log(const std::string& msg) { std::cout << msg << '\n'; }
};
06

Object slicing

Polymorphism needs a pointer or reference. If you copy a derived object into a base-class value, for example by passing it by value, assigning it to a base variable, or storing it in a std::vector<Base>, only the base part is copied. The derived members are "sliced off", and the copy is a genuine base object whose virtual calls go to the base versions. It compiles without a warning.

C++main.cpp
#include <iostream>
#include <memory>
#include <string>
#include <vector>

class Animal {
public:
    virtual ~Animal() = default;
    virtual std::string sound() const { return "..."; }
};

class Dog : public Animal {
public:
    std::string sound() const override { return "Woof"; }
};

void byValue(Animal a)            { std::cout << "by value:     " << a.sound() << '\n'; }
void byReference(const Animal& a) { std::cout << "by reference: " << a.sound() << '\n'; }

int main() {
    Dog rex;
    byValue(rex);                       // copies only the Animal part: sliced
    byReference(rex);                   // refers to the whole Dog

    std::vector<Animal> sliced;
    sliced.push_back(rex);              // stores an Animal, not a Dog
    std::cout << "vector<Animal>: " << sliced[0].sound() << '\n';

    std::vector<std::unique_ptr<Animal>> zoo;
    zoo.push_back(std::make_unique<Dog>());
    std::cout << "vector<unique_ptr<Animal>>: " << zoo[0]->sound() << '\n';
}
Outputcompiled & run with real C++
by value:     ...
by reference: Woof
vector<Animal>: ...
vector<unique_ptr<Animal>>: Woof
Your turn

Make Animal abstract by changing sound to = 0. Now byValue and std::vector<Animal> stop compiling. That is a strong reason to make polymorphic bases abstract: slicing becomes impossible.

Rule of thumb
Pass polymorphic objects as const Base& (or Base&), store them as std::unique_ptr<Base>, and never pass or store them by value. Some teams go further and delete the base's copy operations so slicing cannot compile.
07

final and dynamic_cast

final stops further inheritance. On a class (class CardPayment final : public Payment) nobody can derive from it; on a virtual function (int fee() const final) nobody can override it further. It documents a design decision and lets the compiler skip the virtual lookup when it knows the exact type.

dynamic_cast<Derived*>(basePtr) asks at runtime "is this object really a Derived?" It returns a valid pointer if so and nullptr if not (the reference form, dynamic_cast<Derived&>, throws std::bad_cast instead). It only works on polymorphic classes, those with at least one virtual function. A chain of dynamic_casts to decide what to do is usually a sign that the behaviour should be a virtual function instead; use it for the occasional genuinely type-specific extra.

C++main.cpp
#include <iostream>
#include <memory>
#include <vector>

class Payment {
public:
    virtual ~Payment() = default;
    virtual int fee() const { return 10; }
};

class CardPayment final : public Payment {       // nothing may derive from this
public:
    int fee() const override { return 25; }
    int rewardPoints() const { return 40; }       // only cards have this
};

class UpiPayment : public Payment {
public:
    int fee() const override { return 0; }
};

int main() {
    std::vector<std::unique_ptr<Payment>> batch;
    batch.push_back(std::make_unique<UpiPayment>());
    batch.push_back(std::make_unique<CardPayment>());
    batch.push_back(std::make_unique<Payment>());

    int fees = 0, points = 0;
    for (const auto& p : batch) {
        fees += p->fee();                                           // virtual: no cast needed
        if (auto* card = dynamic_cast<CardPayment*>(p.get())) {     // nullptr if not a card
            points += card->rewardPoints();
        }
    }
    std::cout << "fees " << fees << ", points " << points << '\n';
}
Outputcompiled & run with real C++
fees 35, points 40
Error you will hit

base 'CardPayment' is marked 'final'

C++
class Payment {
public:
    virtual ~Payment() = default;
    virtual int fee() const { return 10; }
};

class CardPayment final : public Payment {
public:
    int fee() const override { return 25; }
};

class PremiumCard : public CardPayment {};

int main() {
    PremiumCard p;
    return p.fee();
}
main.cpp:12:28: error: base 'CardPayment' is marked 'final'
   12 | class PremiumCard : public CardPayment {};
      |                            ^
main.cpp:7:7: note: 'CardPayment' declared here
    7 | class CardPayment final : public Payment {
      |       ^           ~~~~~
main.cpp:16:14: error: no member named 'fee' in 'PremiumCard'
   16 |     return p.fee();
      |            ~ ^
Why the compiler said that

The author of CardPayment declared it final, so deriving from it is rejected. The second error is fallout from the first: once the base is refused, PremiumCard has no members at all. Fix the first error of a cascade and the rest usually disappear.

The fix

Derive from the non-final base (Payment) instead, or, if you own CardPayment and extension is really intended, remove final.

C++
class PremiumCard : public Payment {
public:
    int fee() const override { return 15; }
};
08

Multiple inheritance, the diamond, and composition

A C++ class may have several base classes: class Copier : public Scanner, public Printer. It works well for inheriting several interfaces (abstract classes with no data). It gets awkward when two bases share a common base of their own, the diamond: by default a Copier then contains two separate Device sub-objects, and any use of a Device member is ambiguous.

Error you will hit

non-static member 'id' found in multiple base-class subobjects

C++
#include <iostream>

struct Device {
    int id = 0;
};
struct Scanner : Device {};
struct Printer : Device {};
struct Copier : Scanner, Printer {};

int main() {
    Copier c;
    c.id = 7;
    std::cout << c.id << '\n';
}
main.cpp:12:7: error: non-static member 'id' found in multiple base-class subobjects of type 'Device':
    struct Copier -> Scanner -> Device
    struct Copier -> Printer -> Device
   12 |     c.id = 7;
      |       ^
main.cpp:4:9: note: member found by ambiguous name lookup
    4 |     int id = 0;
      |         ^
Why the compiler said that

There really are two ids in a Copier, one reached through Scanner and one through Printer, and the compiler lists both paths. It cannot know which you mean. (Clang reports line 13 the same way.)

The fix

If the device should exist once, inherit it virtually: struct Scanner : virtual Device and struct Printer : virtual Device. Better still, ask whether inheritance is the right tool at all.

C++main.cpp
#include <iostream>

struct Device {
    int id = 0;
    Device() { std::cout << "Device ctor\n"; }
};
struct Scanner : virtual Device {};     // virtual inheritance: share one Device
struct Printer : virtual Device {};
struct Copier : Scanner, Printer {};

int main() {
    Copier c;
    c.id = 7;                           // no longer ambiguous
    std::cout << "id " << c.id << ", sizeof(Device) " << sizeof(Device) << '\n';
}
Outputcompiled & run with real C++
Device ctor
id 7, sizeof(Device) 4

Only one Device ctor line: the most-derived class (Copier) constructs the single shared Device. Virtual inheritance adds hidden pointers and some complexity, which is why most codebases avoid diamonds with data.

Composition over inheritance

Inheritance is the tightest coupling C++ has: the derived class depends on the base's protected members, constructor and every future change. Before inheriting, ask "is-a or has-a?" A car has an engine; it is not an engine. Composition means holding the other object as a member and calling it. It is easier to change, test and reason about, and it is the default choice in most modern C++ codebases; inheritance is kept for genuine is-a relationships where you need polymorphism.

C++main.cpp
#include <iostream>
#include <memory>
#include <string>

class PaymentGateway {                         // an interface: inherit this
public:
    virtual ~PaymentGateway() = default;
    virtual bool charge(int amount) = 0;
};

class FakeGateway : public PaymentGateway {    // is-a gateway (handy in tests)
public:
    bool charge(int amount) override {
        std::cout << "  charged " << amount << '\n';
        return amount <= 5000;
    }
};

class Checkout {                               // has-a gateway: composition
public:
    explicit Checkout(std::unique_ptr<PaymentGateway> g) : gateway_(std::move(g)) {}
    std::string pay(int amount) {
        return gateway_->charge(amount) ? "paid" : "declined";
    }
private:
    std::unique_ptr<PaymentGateway> gateway_;
};

int main() {
    Checkout checkout(std::make_unique<FakeGateway>());
    std::cout << checkout.pay(1200) << '\n';
    std::cout << checkout.pay(9000) << '\n';
}
Outputcompiled & run with real C++
  charged 1200
paid
  charged 9000
declined

Checkout does not inherit from a gateway; it holds one behind an interface. Swapping in a real gateway, or a different fake in a unit test, needs no change to Checkout. This is the Strategy pattern, covered with the other classic patterns in Design Patterns.

Base / derived class
In class D : public B, B is the base and D is derived. A D contains a complete B.
protected
Access for the class and classes derived from it, but not for outside code.
Virtual function
A member function whose call through a base pointer or reference is dispatched to the object's real type at runtime.
override
Marks a function as overriding a base virtual function. The compiler errors if it does not.
Dynamic dispatch
Choosing which function to run at runtime from the object's dynamic type, usually through a vtable.
vtable
A per-class table of virtual function pointers. Each polymorphic object holds a hidden pointer to its class's table.
Pure virtual function
A virtual function declared = 0, with no implementation required in the base.
Abstract class
A class with at least one pure virtual function. It cannot be instantiated.
Virtual destructor
A destructor declared virtual, so deleting through a base pointer runs every destructor in the chain.
Object slicing
Copying a derived object into a base value, which keeps only the base part and loses the derived behaviour.
final
On a class: no class may derive from it. On a virtual function: no further overrides.
dynamic_cast
A checked runtime cast within a polymorphic hierarchy. Returns nullptr (pointers) or throws std::bad_cast (references) on failure.
Virtual inheritance
class B : virtual A: all paths to A in a diamond share one A sub-object.
Composition
Building a class by holding other objects as members (has-a) instead of inheriting from them (is-a).
Quick check

Base has virtual void f() and void g() (not virtual). Derived defines both. What runs for Base& r = derivedObj; r.f(); r.g();?

Quick check

Why should a base class that is used polymorphically have a virtual destructor?

Frequently asked questions

What is the difference between virtual and pure virtual functions in C++?
A virtual function has an implementation in the base class that derived classes may override. A pure virtual function (= 0) has no required implementation, makes the class abstract so it cannot be instantiated, and must be overridden by every concrete derived class.
What is object slicing in C++?
Copying a derived object into a variable of the base type, for example by passing it by value or storing it in std::vector<Base>. Only the base part is copied, so derived data and overridden behaviour are lost. Pass polymorphic objects by reference and store them as std::unique_ptr<Base> to avoid it.
Should I use inheritance or composition in C++?
Prefer composition (holding an object as a member) unless the relationship is truly is-a and you need to use the derived type through a base pointer or reference. A common middle ground is composition with an abstract interface, so implementations can be swapped without inheritance between concrete classes.

Finish the C++ handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.