Inheritance and Polymorphism
|
This section documents C++23 (ISO/IEC 14882:2024), as published by ISO/IEC JTC1/SC22/WG21 (wg21), verified against the freely available working draft N5046 (eel.is/c++draft) and cppreference.com. This content was generated with the assistance of AI and should be verified against the working draft and cppreference.com before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Public, Protected, and Private Inheritance
class Base {
public:
void publicMethod() {}
protected:
void protectedMethod() {}
private:
void privateMethod() {}
};
class PublicDerived : public Base {}; // is-a: Base's public/protected members keep their access -- the
// overwhelmingly common case, and the only one that models "is-a"
class ProtectedDerived : protected Base {}; // Base's public members become protected in ProtectedDerived
class PrivateDerived : private Base {}; // Base's public/protected members become private -- "implemented
// in terms of", rarely needed since composition usually reads clearer
Default access for class inheritance is private; for struct inheritance it is public — write it
explicitly regardless, so the intent doesn’t depend on the reader remembering the default.
Virtual Functions, override, and final
class Shape {
public:
virtual double area() const = 0; // pure virtual -- makes Shape abstract
virtual ~Shape() = default; // virtual destructor -- see below
};
class Circle : public Shape {
public:
explicit Circle(double r) : radius_(r) {}
double area() const override { return 3.14159265358979 * radius_ * radius_; } // "override" -- compile
// error if this doesn't
// actually override anything
private:
double radius_;
};
class ImmutableCircle final : public Circle { // "final" on the class -- no further subclassing allowed
public:
using Circle::Circle;
double area() const final { return Circle::area(); } // "final" on the method -- no further overriding
};
override catches the classic typo of a slightly wrong signature (a base’s area() const vs. a derived’s
non-const area(), which would silently declare an unrelated new function instead of overriding) as a
compile error instead of a silent bug.
Abstract Classes
A class with at least one pure virtual function (= 0) is abstract — it cannot be instantiated, only used
as a base:
// Shape (above) is abstract because area() is pure virtual.
// Shape s; // error: cannot instantiate abstract class
Shape* s = new Circle(2.0); // fine: through a pointer/reference to the base
delete s; // calls Circle's destructor correctly -- because ~Shape() is virtual
Virtual Destructors and Object Slicing
Deleting a derived object through a non-virtual base-class pointer is undefined behavior — only the base’s
destructor runs, leaking any derived-only resources. Always declare a base’s destructor virtual (or the class
final with no intent to delete polymorphically) once any member function is virtual:
class BadBase {
public:
~BadBase() {} // NOT virtual -- a latent bug waiting for a polymorphic delete
};
class BadDerived : public BadBase {
public:
~BadDerived() { /* release resource */ }
};
// BadBase* p = new BadDerived();
// delete p; // UB: only ~BadBase() runs, BadDerived's cleanup never happens
Object slicing is the related pitfall of assigning a derived object to a base object by value — the
derived-only data is "sliced off", and any virtual call thereafter dispatches to the base’s implementation
(illustrated here with a concrete, non-abstract Shape2 so the by-value parameter itself is legal to declare;
Shape above cannot be sliced this way precisely because it is abstract, and neither can be constructed):
class Shape2 {
public:
virtual double area() const { return 0.0; }
virtual ~Shape2() = default;
};
class Circle2 : public Shape2 {
public:
explicit Circle2(double r) : radius_(r) {}
double area() const override { return 3.14159265358979 * radius_ * radius_; }
private:
double radius_;
};
void processByValue(Shape2 s) { s.area(); } // Shape2 is sliced from whatever derived type was passed in --
// ALWAYS calls Shape2::area(), never Circle2's, no matter what
// was actually passed in
void processByReference(const Shape2& s) { s.area(); } // no slicing: polymorphism works correctly through
// a reference or pointer
dynamic_cast and RTTI
dynamic_cast safely downcasts through a polymorphic hierarchy (one with at least one virtual function),
returning nullptr (for pointers) or throwing std::bad_cast (for references) if the cast is invalid — powered by RTTI (Run-Time Type Information):
#include <memory>
class Square : public Shape {
public:
explicit Square(double side) : side_(side) {}
double area() const override { return side_ * side_; }
private:
double side_;
};
std::unique_ptr<Shape> shape = std::make_unique<Circle>(3.0);
if (auto* circle = dynamic_cast<Circle*>(shape.get())) {
circle->area(); // safe: this really is a Circle
}
if (dynamic_cast<Square*>(shape.get()) == nullptr) {
// safe: shape is not a Square, no crash, just nullptr
}
Needing frequent dynamic_cast down a hierarchy is often a design smell — see
Vocabulary Types (std::variant/std::visit) or
Patterns and Idioms for alternatives that push the
decision into the type system instead of run-time checks.
The Casts
double d = 3.9;
int i = static_cast<int>(d); // checked at compile time; the general-purpose, safest explicit cast
struct A { virtual ~A() = default; };
struct B : A {};
A* a = new B();
B* b = dynamic_cast<B*>(a); // checked at run time via RTTI, as above
const int x = 5;
int& y = const_cast<int&>(x); // strips const -- almost always a code smell; mutating y is UB if
// the original object (x here) is actually const
int n = 65;
char* raw = reinterpret_cast<char*>(&n); // reinterprets bits with essentially no safety net -- for low-level
// byte/pointer manipulation only
Prefer, in order: static_cast for ordinary conversions, dynamic_cast for safe polymorphic downcasts,
const_cast only to interoperate with a const-incorrect API you cannot change, and reinterpret_cast as a last
resort for low-level bit reinterpretation.
Object Layout with a Vtable Pointer
Each polymorphic object carries a hidden vptr pointing at its class’s vtable — an array of function pointers the compiler fills in with the most-derived override of each virtual function. A virtual call indirects through this pointer at run time (the "zero-overhead" cost of polymorphism: one extra pointer per object, one indirection per virtual call).
See Also
-
C: Pointers — C has no virtual functions: the same dispatch is built by hand from structs of function pointers.