C++ Object Slicing: Why Derived State Disappears and How to Prevent It
Key takeaways
Object slicing happens whenever a derived object is copied into a base-typed value: the copy has base size, base vptr, and base behavior. It hides in by-value parameters, containers, catch clauses, and `throw e;`. Pass by reference, store smart pointers, and make polymorphic base copies protected so the compiler rejects slicing.
What object slicing actually is
When you copy a derived object into an object whose type is the base class, only the base subobject is copied. The derived-only data members and the derived behavior have nowhere to go, so they are dropped. The result is a perfectly valid, fully formed Base object — which is exactly why slicing is dangerous: nothing crashes, the program just quietly does the wrong thing.
This article explains the mechanism and how to design base classes so slicing cannot compile. If you are chasing a bug where an override has stopped running and want a checklist of places to look in an existing codebase (by-value parameters, auto copies, factory helpers, clone), see C++ Slicing Bugs: Finding Where a Base-Type Copy Dropped Your Derived Object.
struct Base {
int x = 0;
virtual ~Base() = default;
};
struct Derived : Base {
int y = 0;
};
Derived d;
d.x = 1;
d.y = 2;
Base b = d; // calls Base's copy constructor with a const Base& bound to d
// b.y does not exist: b has sizeof(Base) and static *and* dynamic type Base
The mechanism is simple once you look at what the compiler sees. Base b = d; means “construct a Base from d”. The only constructor that fits is Base(const Base&), and a const Base& binds happily to the Base part of a Derived. That copy constructor knows nothing about y, so y is never copied, and storage for it is never reserved.
Why virtual functions “stop working”
The most common complaint is “my override isn’t being called”. Dispatch is not broken. A by-value Base object is constructed by Base’s copy constructor, and that constructor sets the object’s vptr to Base’s vtable. The object’s dynamic type is Base, so a virtual call resolves to Base::speak. The Dog you passed in still exists in the caller, but the function is operating on a different object.
#include <iostream>
#include <string>
class Animal {
public:
virtual ~Animal() = default;
virtual std::string speak() const { return "..."; }
};
class Dog : public Animal {
public:
explicit Dog(std::string name = "Rex") : name_(std::move(name)) {}
std::string speak() const override { return name_ + ": Woof!"; }
private:
std::string name_;
};
void byValue(Animal a) { std::cout << a.speak() << '\n'; } // sliced copy
void byRef(const Animal& a) { std::cout << a.speak() << '\n'; } // the real Dog
int main() {
Dog d("Rex");
byValue(d); // prints "..."
byRef(d); // prints "Rex: Woof!"
}
References and pointers do not copy anything; they refer to the complete object, so the vptr they see is Dog’s. That is the whole fix for function parameters: take const Animal& (or Animal& / Animal* when mutation or nullability is intended).
Where slicing hides
The by-value parameter is the textbook case, and code review usually catches it. The versions below are the ones that survive review.
Containers of base values
std::vector<Animal> zoo;
zoo.push_back(Dog("Rex")); // constructs an Animal from the Dog
zoo[0].speak(); // "..."
std::vector<Animal> stores elements of exactly sizeof(Animal). There is no way for it to hold a Dog. The owning alternative is a vector of smart pointers:
std::vector<std::unique_ptr<Animal>> zoo;
zoo.push_back(std::make_unique<Dog>("Max"));
zoo[0]->speak(); // "Max: Woof!"
If the set of concrete types is closed and known up front, std::variant<Dog, Cat, Bird> is often better: values stay contiguous, there is no heap allocation per element, and dispatch goes through std::visit. See std::variant for that pattern and type erasure for value-semantic wrappers that own the full object.
Returning a base by value
Animal makeAnimal() {
return Dog("Rex"); // the Dog is a temporary; an Animal is built from it
}
Factories that return polymorphic objects should return std::unique_ptr<Animal>. The derived object then lives on the heap with its full type, and ownership is explicit.
Catching exceptions by value
This one bites people who otherwise know better, because exception hierarchies are polymorphic and handlers are written quickly.
struct ParseError : std::runtime_error {
ParseError(const std::string& msg, int line)
: std::runtime_error(msg), line(line) {}
int line;
};
try {
throw ParseError("bad token", 42);
} catch (std::runtime_error e) { // copy of the runtime_error part only
// dynamic_cast<ParseError*>(&e) is always nullptr here; e.line is gone
}
GCC catches this one for you. With -Wall, g++ reports:
warning: catching polymorphic type 'class std::runtime_error' by value [-Wcatch-value=]
The rule is “throw by value, catch by const&”.
Rethrowing with throw e;
Even with a reference in the handler, rethrowing the named variable slices:
void load() {
try {
throw std::out_of_range("index 7");
} catch (const std::exception& e) {
log(e.what());
throw e; // throws a NEW object copied from the static type: std::exception
}
}
A handler further up that catches std::out_of_range will no longer match, and with libstdc++ what() on the sliced copy returns the generic string "std::exception" instead of "index 7", because the message lived in the derived part. A bare throw; rethrows the original object and keeps everything intact.
I have seen this pattern produce one of the more confusing failure modes in C++: a logging wrapper added around a call site with the best of intentions, and suddenly every error surfacing in the top-level handler says “std::exception” with no detail. The wrapper looked harmless in review because it caught by reference; the slicing was in the one-word difference between throw e; and throw;.
Partial assignment through a base reference
Slicing also happens on assignment, and here it can corrupt an object instead of just losing data:
Derived a; a.x = 1; a.y = 1;
Derived b; b.x = 2; b.y = 2;
Base& ref = a;
ref = b; // calls Base::operator=, which copies only x
// a is now x == 2, y == 1: half of b, half of the old a
a is still a Derived, but its state is a mix of two objects. If Derived maintains an invariant between its own members and the base members (a cached value, a size that must match a buffer), that invariant is now broken with no diagnostic.
Making slicing a compile error
Advice like “remember to pass by reference” does not scale across a team. The durable fix is to make the base class impossible to copy from outside the hierarchy. The C++ Core Guidelines (C.67) put it directly: a polymorphic class should suppress public copy/move.
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
virtual std::unique_ptr<Shape> clone() const = 0;
protected:
Shape() = default;
Shape(const Shape&) = default; // derived classes can still copy
Shape& operator=(const Shape&) = default;
Shape(Shape&&) = default;
Shape& operator=(Shape&&) = default;
};
class Circle final : public Shape {
public:
explicit Circle(double r) : r_(r) {}
double area() const override { return 3.14159265 * r_ * r_; }
std::unique_ptr<Shape> clone() const override {
return std::make_unique<Circle>(*this); // uses Circle's copy ctor
}
private:
double r_;
};
Two details matter here:
- Protect the move operations too. A common mistake is
Base(const Base&) = delete;together with a publicBase(Base&&) = default;. That still allowsBase b = std::move(derived);, which slices just as badly. Declaring copy/moveprotectedblocks both from outside while letting derived classes use them to implement their own copies. - Offer
clone()when callers need a copy. Once public copying is gone, a virtualclone()gives callers a way to duplicate an object without knowing its concrete type.
clone() has its own slicing trap, which is why Circle is marked final above. If a class Ellipse overrides clone() and Circle later derives from Ellipse without overriding it again, circle.clone() quietly calls Ellipse::clone() and returns a copy of the Ellipse part: slicing again, one level down, and no compiler warning. Two defences are common. Keep leaf classes final, so a new level of the hierarchy is a deliberate change. Or generate clone() with a CRTP helper (class Circle : public Cloneable<Circle, Ellipse>) so every concrete class gets a correct override by construction. A unit test that checks typeid(*obj->clone()) == typeid(*obj) for each concrete type catches the omission too.
Note also that covariant return types only work with raw pointers and references. std::unique_ptr<Circle> clone() const override does not compile as an override of std::unique_ptr<Shape> clone() const, because unique_ptr<Circle> and unique_ptr<Shape> are unrelated types to the compiler. Returning std::unique_ptr<Shape> everywhere, as above, is the simple answer.
With a non-abstract base, the same trick turns silent slicing into an error:
error: 'constexpr Concrete::Concrete(const Concrete&)' is protected within this context
Detection beyond the compiler
GCC has no general “slicing” warning — -Weffc++, sometimes suggested for this, does not detect it. What does help:
-Wallin GCC enables-Wcatch-value, which covers the catch-by-value case.- clang-tidy has
cppcoreguidelines-slicing, which flags copies that drop derived members or overridden virtuals. - In the debugger, a sliced object shows up with a base-class vptr. If
p *thisin GDB shows_vptr.Animal = 0x... <vtable for Animal+16>where you expected aDog, you are looking at a copy.
The case where slicing is harmless is worth naming too: if the base is a plain value type that is intentionally used as a “view” of the derived data (for example, copying the Point part out of a ColoredPoint for a geometry function), and there are no virtual functions involved, the copy is doing exactly what you asked. Slicing is a bug when the object is meant to be used polymorphically.
The trap I have watched teams fall into repeatedly is the refactor from std::vector<std::unique_ptr<Base>> to std::vector<Base> “to get rid of the heap allocations”. It compiles cleanly, tests that only check counts or base fields still pass, and the derived behavior silently disappears. If a base class had protected copy operations from the start, that refactor would have failed at the first push_back.
Where slicing happens and the fix
| Situation | Slices? | Fix |
|---|---|---|
void f(Base b) | Yes | const Base& |
Base make() { return Derived(); } | Yes | return std::unique_ptr<Base> |
std::vector<Base> | Yes | vector<unique_ptr<Base>> or std::variant |
catch (Base e) | Yes | catch (const Base& e) |
throw e; in a handler | Yes | throw; |
Base& r = d1; r = d2; | Partially | protected assignment in base |
Base& r = d; / Base* p = &d; | No | — |