C++ Slicing Bugs: Finding Where a Base-Type Copy Dropped Your Derived Object
Key takeaways
What object slicing is, why the copy constructor of the base class causes it, the five places it hides (by-value parameters, vector<Base>, catch by value, assignment through a base reference, and auto copies), and how to turn it into a compile error with abstract bases or a protected copy constructor plus a virtual clone().
The symptom: the override silently stops running
struct Animal {
virtual ~Animal() = default;
virtual std::string speak() const { return "..."; }
};
struct Dog : Animal {
std::string name;
explicit Dog(std::string n) : name(std::move(n)) {}
std::string speak() const override { return name + " barks"; }
};
void byValue(Animal a) { std::cout << a.speak() << '\n'; }
void byRef(const Animal& a) { std::cout << a.speak() << '\n'; }
int main() {
Dog d("Buddy");
byValue(d); // prints: ...
byRef(d); // prints: Buddy barks
}
There is no error and, with GCC 10 at -Wall -Wextra, no warning. The only difference between the two functions is one &, and one of them has quietly lost everything that made the object a Dog.
Why it happens
byValue needs to construct a brand-new Animal object for its parameter. The only way to build an Animal from d is Animal’s copy constructor, Animal(const Animal&). A Dog binds happily to const Animal&, because a Dog is an Animal, and the copy constructor copies what it knows about: the Animal part. The name member is not part of an Animal, so it is not copied. It is “sliced off”.
The new object is not a damaged Dog. It is a complete, valid Animal, and its vtable pointer is set by Animal’s constructor to Animal’s vtable. That is why virtual calls stop dispatching to Dog: they are correctly dispatching on the dynamic type of the object you actually have, which is now Animal. Polymorphism only works through references and pointers, because only they can refer to an object whose dynamic type differs from the static type.
This is also why the compiler cannot help by default. Nothing is wrong with the conversion in the language’s terms. It is exactly as legal as double x = 3;.
Where slicing hides
By-value parameters
The example above. Any function taking a polymorphic base by value should be treated as a bug. Use const Base& to read, Base& to modify, or a smart pointer to take ownership.
Containers of base type
std::vector<Animal> zoo;
zoo.push_back(Dog("Rex"));
std::cout << zoo[0].speak(); // prints: ...
A std::vector<Animal> stores Animals: each slot is exactly sizeof(Animal) bytes, so there is physically no room for a Dog’s extra members. Store std::vector<std::unique_ptr<Animal>>, or if the set of types is closed, a std::vector<std::variant<Dog, Cat>>, which stores values without inheritance at all.
Catching exceptions by value
try {
throw std::runtime_error("disk full");
} catch (std::exception e) {
std::cout << e.what(); // prints: std::exception
}
The handler copy-constructs a plain std::exception, and the message is gone. GCC does warn about this one under -Wall:
warning: catching polymorphic type 'class std::exception' by value [-Wcatch-value=]
Always catch (const std::exception& e), and rethrow with a bare throw; rather than throw e;, which throws a sliced copy of the static type.
Assignment through a base reference
This is the variant that corrupts objects rather than just losing data:
struct P { int x; P(int x) : x(x) {} virtual ~P() = default; };
struct Q : P { int y; Q(int x, int y) : P(x), y(y) {} };
Q a(1, 2), b(3, 4);
P& ra = a;
ra = b;
std::cout << a.x << ' ' << a.y; // prints: 3 2
P::operator= copies only the P part. a is left with b’s x and its own old y: a mix of two objects that neither was ever in. If the derived class maintains an invariant between its members and its base’s members, that invariant is now broken with no error anywhere.
auto copies and range-for
auto x = *basePtr; deduces Base and slices. So does for (Animal a : animalRefs) when iterating something that yields Animal&. Use auto& / const auto&.
Factory and wrapper helpers that copy by base type
Any helper that constructs a new object of the base type from an existing one slices, even when it looks like it is creating a smart pointer. std::make_shared<Animal>(dog) and std::make_unique<Animal>(dog) call Animal(const Animal&), so the resulting pointer owns a freshly built Animal, not a Dog. The same goes for std::optional<Animal> opt = dog; and std::any a = static_cast<Animal&>(dog);: each stores a value of the static type. The pattern to look for is a template argument or a declared type that names the base while the source is a derived object. If you want a pointer to the existing object, take its address or move it into a std::make_unique<Dog>(...); if you want a copy of its real type, call clone().
Turning slicing into a compile error
Relying on everyone to remember & does not scale, so the durable fix is structural. The full base-class design (protected copy and move operations, why = delete on copy alone still lets Base b = std::move(d); slice, and the C++ Core Guidelines rule behind it) is covered in C++ Object Slicing: Why Derived State Disappears. For triage, two facts matter:
- An abstract base makes cases 1 and 2 fail to compile (
error: cannot declare parameter 'a' to be of abstract type 'Animal'), because noAnimalobject can exist. It does not catch case 4, since assignment through a reference creates no newAnimal. - A protected copy constructor and copy assignment in the base also block case 4 from outside the hierarchy, while derived classes can still use them to copy themselves. The error you will then see at the old call site is
'constexpr Animal::Animal(const Animal&)' is protected within this context, which points straight at the line that used to slice.
Copying polymorphic objects correctly: clone()
Once you stop copying through the base type, you still sometimes need a copy of “whatever this object really is”. That is the job of a virtual clone:
struct Dog : Animal {
std::string name;
explicit Dog(std::string n) : name(std::move(n)) {}
std::string speak() const override { return name + " barks"; }
std::unique_ptr<Animal> clone() const override {
return std::make_unique<Dog>(*this); // Dog's own copy constructor
}
};
std::unique_ptr<Animal> a = std::make_unique<Dog>("Rex");
auto b = a->clone();
std::cout << b->speak(); // prints: Rex barks
Each class copies itself using its own copy constructor, so nothing is lost. The cost is one override per class; forgetting it in a derived class silently clones the nearest parent that implements it, so make clone() pure in the base to force each concrete class to write one.
A pure clone() in the root only forces the first level of concrete classes to implement it. If Puppy derives from Dog and does not override clone(), Dog::clone() is inherited and puppy.clone() returns a Dog, which is slicing again one level down. Two common defenses are a debug assertion inside each clone() that typeid(*result) == typeid(*this), and a CRTP helper (struct Dog : Cloneable<Dog, Animal>) that generates the override from the class’s own type so it cannot be forgotten.
clone() returns std::unique_ptr<Animal> rather than std::unique_ptr<Dog> because C++ covariant return types work only with raw pointers and references, not with smart pointers. If callers that hold a concrete Dog need a Dog back, add a non-virtual cloneDog() in Dog, or simply copy-construct Dog directly; there is no slicing when the static type is already the complete type.
Finding slicing in an existing codebase
Because the compiler accepts slicing silently, finding it after the fact takes tooling. clang-tidy’s cppcoreguidelines-slicing check flags copies of a derived object into a base object when the derived class adds data members or overrides virtual functions, which covers most of the cases above. GCC’s -Wcatch-value handles the exception case. For the rest, it is worth searching for by-value parameters and std::vector< element types that name a class with virtual functions; a class with a virtual destructor is almost never meant to be passed by value.
A runtime symptom that points to slicing is a virtual function returning base-class behavior for an object you are sure is derived. In a debugger, print the object: GDB shows the dynamic type through the vtable with set print object on, and a sliced object reports the base type because that is what it really is.
Where this goes wrong in practice
The case I find hardest to spot in review is a refactor that changes a parameter from const Shape& to Shape “because it is small now” or because a static analysis tool suggested passing cheap types by value. The code compiles, most tests pass because they construct plain base objects, and the one path that passed a derived object starts producing default behavior. Nothing points at the changed signature, because the failure shows up as wrong output far away.
The second is std::vector<Base> in code that was not polymorphic when it was written. A class gains its first derived type months later, someone pushes the new type into the existing vector, and the new behavior simply never happens. Making every polymorphic base abstract, or its copy constructor protected, turns both of these into compile errors on the day they are introduced instead of bugs found later.
FAQ
Q. Can catching an exception by value cause slicing?
A. Yes. catch (std::exception e) copies into a plain std::exception, so what() loses the derived message. Catch by const& and rethrow with throw;. GCC’s -Wcatch-value, enabled by -Wall, flags it.
Q. Is slicing ever intentional?
A. Occasionally, when you really do want only the base data, such as extracting a plain value-type base from a richer object. Make that explicit with a named function (toBase()) so readers do not have to guess whether the copy was deliberate.