C++ / INHERITANCE AND POLYMORPHISM
Object slicing and why value semantics break
Recognise object slicing in parameters, containers and assignments, and prevent it with references, smart pointers and a virtual clone.
What you will learn
- Explain why copying a Derived into a Base variable drops its data and its overrides
- Pass polymorphic objects as const Base& and store them as unique_ptr<Base>
- Spot slicing in by-value parameters, vector<Base>, and catch (Exception e)
- Prevent slicing with an abstract base or protected copy plus a virtual clone()
Understanding Object slicing and why value semantics break
A variable's type decides how many bytes it occupies, and that decision is made at compile time. `Shape s` is exactly sizeof(Shape) bytes wide, so when you copy-initialise it from a `Square`, the compiler selects `Shape`'s copy constructor and copies the `Shape` subobject of the argument; the `side` member has no storage in the destination and is simply not copied. Nothing about this is illegal, because public inheritance makes the Square-to-Shape conversion implicit, so the compiler stays silent while half the object is discarded. That is object slicing: the derived layers are cut away from the copy.
Losing the data is only half the damage. The surviving object has dynamic type `Shape`, because it was built by `Shape`'s constructor and carries `Shape`'s vptr, so `s.area()` runs `Shape::area` even though `area` is virtual; the slice happened during the copy, before dispatch could ever look at a Square. This is the real conflict: value semantics assume the declared type tells you an object's size and behaviour, while polymorphism assumes the actual object may be bigger and behave differently. Both assumptions cannot hold for the same storage, which is why polymorphic types are handled through references and pointers, which never need to know the size.
Assignment breaks in the mirror-image way. `baseRef = someBase` calls the non-virtual `Base::operator=` chosen by the static type, overwriting the base fields of a derived object while leaving its derived fields at their old values, producing a mixture no constructor could ever have created. Since both problems require base-typed storage to exist in the first place, the durable fix is structural: keep the base abstract or give it protected copy operations so `Base b = derived;` does not compile, and add a virtual `clone()` returning `unique_ptr<Base>` for the cases where a copy is genuinely needed.
<iostream>
struct Shape {
int sides = 0;
virtual double area() const { return 0.0; }
virtual ~Shape() = default;
};
struct Square : Shape {
double side;
explicit Square(double s) : side(s) { sides = 4; }
double area() const override { return side * side; }
};
void printByValue(Shape s) { // copies only the Shape subobject
std::cout << "by value: sides=" << s.sides << " area=" << s.area() << '\n';
}
void printByRef(const Shape& s) { // binds to sq itself, no copy
std::cout << "by ref: sides=" << s.sides << " area=" << s.area() << '\n';
}
int main() {
Square sq(3.0);
printByRef(sq);
printByValue(sq);
Shape copy = sq; // slicing copy-initialisation
std::cout << "copy: sides=" << copy.sides << " area=" << copy.area() << '\n';
}
A copy is always made into the destination's static type, so a base-typed copy of a derived object retains only the base subobject and only the base's virtual behaviour.
Worked examples
Containers of values slice, containers of pointers do not
Shows why std::vector<Base> destroys polymorphism while std::vector<std::unique_ptr<Base>> keeps it.
<iostream>
<memory>
<vector>
struct Animal {
virtual const char* speak() const { return "..."; }
virtual ~Animal() = default;
};
struct Dog : Animal {
const char* speak() const override { return "woof"; }
};
int main() {
Dog d;
std::vector<Animal> flat;
flat.push_back(d); // element type is Animal
std::vector<std::unique_ptr<Animal>> poly;
poly.push_back(std::make_unique<Dog>()); // element type is a pointer
std::cout << "value element: " << flat[0].speak() << '\n';
std::cout << "pointer element: " << poly[0]->speak() << '\n';
}
Example explained
Line 1`std::vector<Animal>` allocates slots of exactly sizeof(Animal), so push_back can only copy the Animal subobject of `d`.
Line 2`flat[0]` is a genuine Animal, so `speak()` dispatches to Animal::speak and prints the base string.
Line 3In the second vector the Dog is allocated on the heap and only an 8-byte pointer lives in the container, so nothing is truncated.
Line 4`poly[0]->speak()` calls through a pointer whose target is still a Dog, so the override runs.
Partial assignment through a base reference
Assigning through Base& overwrites only the base fields, leaving an object whose halves disagree.
<iostream>
struct Base {
int a = 1;
virtual ~Base() = default;
};
struct Derived : Base {
int b = 2;
void show() const { std::cout << "a=" << a << " b=" << b << '\n'; }
};
int main() {
Derived d;
d.a = 10;
d.b = 20;
d.show();
Base plain; // a = 1
Base& r = d; // r names the Base part of d
r = plain; // Base::operator= copies the Base part only
d.show();
Derived other; // a = 1, b = 2
d = other; // Derived::operator= copies both parts
d.show();
}
Example explained
Line 1`Base& r = d;` copies nothing, so at this point `d` is still intact.
Line 2`r = plain;` picks `Base::operator=` from the static type `Base`; copy assignment is not virtual, so only `a` is written.
Line 3After that line `a` came from `plain` and `b` is a leftover from before, a state no constructor of Derived can produce.
Line 4`d = other;` goes through Derived's own copy assignment, which assigns the base subobject and then `b`, so both fields agree again.
Make slicing impossible, copy with clone()
An abstract base plus protected copy operations rejects base-typed values at compile time, and clone() copies with knowledge of the dynamic type.
<iostream>
<memory>
class Shape {
public:
virtual double area() const = 0;
virtual std::unique_ptr<Shape> clone() const = 0;
virtual ~Shape() = default;
protected:
Shape() = default;
Shape(const Shape&) = default; // usable by derived classes only
Shape& operator=(const Shape&) = default;
};
class Rect : public Shape {
double w, h;
public:
Rect(double width, double height) : w(width), h(height) {}
double area() const override { return w * h; }
std::unique_ptr<Shape> clone() const override {
return std::make_unique<Rect>(*this);
}
};
int main() {
Rect r(2.0, 5.0);
const Shape& s = r;
// Shape sliced = s; // error: Shape is abstract
// std::vector<Shape> v; // same error at instantiation
std::unique_ptr<Shape> p = s.clone();
std::cout << "area " << p->area() << '\n';
}
Example explained
Line 1Two pure virtual functions make `Shape` abstract, so no Shape object can exist and the commented-out slicing lines are compile errors, not silent bugs.
Line 2The protected copy constructor and copy assignment are the fix when a base must stay concrete: derived classes can still copy their base part, outside code cannot.
Line 3`make_unique<Rect>(*this)` runs inside Rect, where the static type is known, so Rect's copy constructor copies `w` and `h` as well.
Line 4The caller gets a `unique_ptr<Shape>` and therefore never holds a Shape by value.
Important notes
Moving does not rescue you: `Base b = std::move(d);` slices exactly like a copy, because `b` is still a Base being initialised from the Base subobject.
No mainstream compiler rejects or even warns about slicing by default, since the derived-to-base conversion is legal; the check lives in tooling such as clang-tidy's cppcoreguidelines-slicing (Core Guidelines ES.63).
Common mistakes
Writing `void draw(Shape s)` and trusting `virtual` to sort it out: the parameter is a Shape, so `Shape::area` runs and the Square's side length was already thrown away at the call site.
Filling a `std::vector<Shape>` with Squares: every push_back truncates the element, so areas read back as 0 and no compiler diagnostic points at the container.
Catching by value with `catch (std::exception e)`: the thrown object is copied into a base exception, so an overridden `what()` and extra data such as an error code are lost before the handler runs.
Try it yourself
Change, predict, then run
Add a `std::vector<Shape>` to the main example, push a `Square(3.0)` into it and print `v[0].area()`. Then change the container to `std::vector<std::unique_ptr<Shape>>` holding `std::make_unique<Square>(3.0)` and watch the printed area change from 0 to 9.
Open the C++ workspaceCheck your understanding
`void draw(Shape s)` is called with a `Square` argument, where `Shape::area` is virtual and `Square` overrides it. Which `area` runs inside `draw`, and why?
- Square::area, because area is virtual and virtual calls are always resolved at run time
- Shape::area, because `s` is a separate Shape object created by Shape's copy constructor at the call, so its dynamic type is Shape
- Square::area, because the argument's dynamic type is Square and that type travels with the value
- Neither: passing a Square where a Shape parameter is expected is a compile error
Show answer
Virtual dispatch uses the dynamic type of the object you actually call through, and `s` is a fresh Shape built by Shape's copy constructor from the Square's Shape subobject, so its dynamic type is Shape and `Shape::area` runs. Option 0 is tempting because `area` really is virtual, but dispatch can only reach `Square::area` when the object being called on is part of a Square, and this parameter is not; option 3 is wrong because public inheritance makes the conversion implicit and legal, which is precisely why the bug is silent. Declaring the parameter `const Shape&` binds to the original Square and calls the override.