In C++, inheritance enables derived classes to extend or override behavior from a base class. But to use inheritance correctly — especially when dealing with polymorphism — you must use pointers or references, not values. This article explains why object slicing occurs, why destructors should be virtual, and how smart pointers help manage lifetimes.
Object Slicing: What It Really Means
Consider the following classes:
class Base {
public:
virtual std::string type() const { return "Base"; }
};
class Derived : public Base {
public:
std::string type() const override { return "Derived"; }
std::string derived_info = "extra";
};
Now do this:
Derived d;
Base b = d;
std::cout << b.type(); // prints "Base"
Although d
is a Derived
object, the assignment Base b = d
performs a copy into a Base
object. This invokes the Base
copy constructor, which only copies the Base
portion of d
. Everything defined in Derived
— data and overrides — is sliced off.
This is called object slicing. After slicing, the object is just a pure Base
, and virtual dispatch no longer works as expected.
How to Avoid Slicing
Use a pointer or reference to access the object:
Derived d;
Base* ptr = &d;
std::cout << ptr->type(); // prints "Derived"
Base& ref = d;
std::cout << ref.type(); // also prints "Derived"
These avoid copying entirely. Instead, they access the original Derived
object polymorphically. Thanks to the virtual table (vtable), method calls like type()
are dispatched at runtime to the appropriate override.
Why Use std::shared_ptr
or std::unique_ptr
?
Raw pointers work, but they come with manual lifetime management. Smart pointers solve that:
std::shared_ptr<Base> b = std::make_shared<Derived>();
std::cout << b->type(); // prints "Derived"
shared_ptr
ensures the object is kept alive as long as needed, preventing memory leaks and dangling pointers. Similarly, unique_ptr
provides exclusive ownership with automatic destruction.
Why the Base Class Needs a Virtual Destructor
Consider this:
class Base {
public:
~Base() { std::cout << "Base destroyed\n"; }
};
class Derived : public Base {
public:
~Derived() { std::cout << "Derived destroyed\n"; }
};
Base* b = new Derived();
delete b; // Only Base::~Base() runs!
This code has a bug. Since Base
's destructor is not virtual, deleting via a Base*
only calls Base
's destructor. Derived
's destructor is skipped entirely.
This is dangerous: Derived
may hold resources like heap allocations or file handles that need to be cleaned up. Skipping its destructor can cause leaks or undefined behavior.
Correct Version:
class Base {
public:
virtual ~Base() { std::cout << "Base destroyed\n"; }
};
class Derived : public Base {
public:
~Derived() { std::cout << "Derived destroyed\n"; }
};
Base* b = new Derived();
delete b; // Calls both Derived and Base destructors
By making the destructor virtual, the call to delete b;
uses the vtable to determine the correct destructor sequence, ensuring both destructors run in the correct order.
Why This Works: Static vs Dynamic Dispatch
- Assignment:
Base b = d;
triggers aBase
copy constructor. The compiler doesn't see or copy theDerived
part. - Virtual calls: only resolve dynamically through
Base*
orBase&
. You need a vtable, which requires virtual methods. - Destruction: if the destructor isn't virtual,
delete base_ptr
won't look up the correct derived destructor at runtime.
Summary
- Object slicing happens when assigning a derived object to a base object by value — only the base part is kept.
- Use pointers or references to access polymorphic objects without slicing.
- Smart pointers like
shared_ptr
orunique_ptr
simplify and safeguard object lifetime management. - Always declare the base class destructor as virtual if you intend to use inheritance. Otherwise, deleting via base pointers is unsafe.
These practices ensure inheritance works as intended and avoids subtle, dangerous bugs.