Lesson 6 of 9
A Manager is an Employee. So this compiles:
void report(Employee e); // by value
Manager m(...);
report(m); // fine — a Manager is an Employee
And it is wrong. The Manager is copied into an Employee-sized box, and everything that made it a
Manager is discarded on the way in.
This is slicing, and it compiles without a single warning.
The demonstration
#include <iostream>
#include <memory>
#include <string>
#include <vector>
class Employee {
protected:
std::string name_;
double base_;
public:
Employee(std::string n, double b) : name_(std::move(n)), base_(b) {}
virtual ~Employee() = default;
virtual double pay() const { return base_; }
virtual std::string role() const { return "employee"; }
const std::string& name() const { return name_; }
};
class Manager : public Employee {
double bonus_;
public:
Manager(std::string n, double b, double bonus)
: Employee(std::move(n), b), bonus_(bonus) {}
double pay() const override { return base_ + bonus_; }
std::string role() const override { return "manager"; }
};
// SLICES: the parameter is a value, so only the Employee part is copied.
void report_by_value(Employee e) {
std::cout << " by value: " << e.name() << " is a " << e.role()
<< ", paid " << e.pay() << "\n";
}
// Correct: a reference refers to the whole object.
void report_by_ref(const Employee& e) {
std::cout << " by reference: " << e.name() << " is a " << e.role()
<< ", paid " << e.pay() << "\n";
}
int main() {
Manager m("Dara", 5000, 2000);
std::cout << "one Manager, two ways of passing it:\n";
report_by_value(m);
report_by_ref(m);
std::cout << "\nsizes:\n";
std::cout << " sizeof(Employee): " << sizeof(Employee) << "\n";
std::cout << " sizeof(Manager): " << sizeof(Manager) << "\n";
std::cout << "\na vector of Employee holding Managers:\n";
std::vector<Employee> sliced;
sliced.push_back(m);
sliced.push_back(Manager("Bopha", 6000, 3000));
for (const Employee& e : sliced) {
std::cout << " " << e.name() << ": " << e.role() << ", " << e.pay() << "\n";
}
std::cout << "\na vector of unique_ptr<Employee>:\n";
std::vector<std::unique_ptr<Employee>> correct;
correct.push_back(std::make_unique<Manager>("Dara", 5000, 2000));
correct.push_back(std::make_unique<Manager>("Bopha", 6000, 3000));
for (const auto& e : correct) {
std::cout << " " << e->name() << ": " << e->role() << ", " << e->pay() << "\n";
}
return 0;
}
one Manager, two ways of passing it:
by value: Dara is a employee, paid 5000
by reference: Dara is a manager, paid 7000
sizes:
sizeof(Employee): 48
sizeof(Manager): 56
a vector of Employee holding Managers:
Dara: employee, 5000
Bopha: employee, 6000
a vector of unique_ptr<Employee>:
Dara: manager, 7000
Bopha: manager, 9000
The same object, passed two ways, reports two different salaries. 5000 and 7000, differing by
exactly the bonus that Manager added and the Employee copy never received.
The sizes say why. Manager is 56 bytes; Employee is 48. Copying a Manager into an Employee
parameter copies 48 of those bytes and drops 8 — the bonus_ member. There is nowhere to put it.
Note that role() returned "employee" too. The copy is genuinely an Employee: its vptr points
at Employee's vtable, because that is what was constructed. Slicing does not produce a broken
Manager; it produces a real Employee that happens to have been built from part of one. Virtual
dispatch then works perfectly and gives the wrong answer.
Where it happens
Four places, and the third is the one that catches whole codebases:
// 1. Passing by value
// void f(Base b); f(derived); // sliced
//
// 2. Returning by value
// Base make() { return Derived(); } // sliced
//
// 3. Storing in a container of values
// std::vector<Base> v; v.push_back(derived); // sliced
//
// 4. Assigning to a base object
// Base b; b = derived; // sliced
The std::vector<Employee> in the output above sliced both managers. A container of a polymorphic
base type is almost always a bug, and it is an easy one to write because it looks so reasonable.
The fix
Pass and return by reference or pointer:
// void report(const Employee& e); // no copy, full object
// void modify(Employee& e);
// const Employee& choose(...); // watch the lifetime (lesson 8.5)
Store polymorphic objects indirectly:
// std::vector<std::unique_ptr<Employee>> — owning, the usual choice
// std::vector<std::shared_ptr<Employee>> — when ownership is shared
// std::vector<Employee*> — non-owning, if lifetimes are clear
// std::vector<std::reference_wrapper<Employee>> — non-owning references
The unique_ptr version in the output reported 7000 and 9000 — correct, because nothing was copied.
Making slicing impossible
The language will not warn you, so the defence has to be in the class. Three options, strongest first.
Make the base abstract (lesson 16.4). An abstract class cannot be instantiated, so there is no
Employee object for a Manager to be sliced into:
// class Employee {
// public:
// virtual double pay() const = 0; // now abstract
// };
//
// void report(Employee e); // error: parameter type is abstract
// std::vector<Employee> v; // error
This is the best answer when it fits, and it usually does — a base that is never instantiated directly should say so.
Delete the copy operations in the base:
// class Employee {
// public:
// Employee(const Employee&) = delete;
// Employee& operator=(const Employee&) = delete;
// protected:
// Employee(Employee&&) = default; // derived classes may still move
// };
Now the slice does not compile. The cost is that legitimate copies of the base are also gone, which is usually fine for a polymorphic type.
Make the copy operations protected:
// protected:
// Employee(const Employee&) = default;
// Employee& operator=(const Employee&) = default;
Derived classes can still copy their base part — which they need for their own copy constructors —
but outside code cannot slice. This is the standard recipe for a copyable polymorphic hierarchy, and
it pairs with a virtual clone() (lesson 16.5) for the public copying interface.
Assignment slicing is worse
// Employee e("Kosal", 4000);
// Manager m("Dara", 5000, 2000);
// e = m; // e is still an Employee, now holding Dara's base salary
// m = e; // does NOT compile if Manager has no operator=(const Employee&)
The first line is the usual slice. The second is worth knowing about: assigning a base to a derived is rejected, because there is no way to produce the missing members. So slicing is asymmetric — it silently succeeds in the dangerous direction and fails loudly in the safe one.
If Manager does have an implicitly generated operator=, e = m uses Employee::operator=,
which copies only the Employee part. Nothing is corrupted, and nothing is correct either.
No compiler will tell you
Compiling this lesson's program with -Wall -Wextra -Wpedantic -Wconversion -Wshadow produces no
diagnostic at all. The code is entirely legal C++: a Manager converts to an Employee, and a
copy constructor exists.
Clang has -Wslicing in some configurations; GCC 13 has no equivalent. Do not plan on the compiler
catching this — plan on the class design preventing it.
Try it
Build a Shape hierarchy and demonstrate slicing in all four places it occurs. Then apply each of
the three defences in turn and record which of the four cases each one blocks.
Checkpoint
- Slicing copies a derived object into a base-sized object, discarding the derived part.
- Measured: the same
Managerreported 5000 by value and 7000 by reference — 56 bytes into 48. - The result is a genuine base object, so virtual dispatch works and gives the base's answer.
- It happens on pass-by-value, return-by-value, containers of values, and base-from-derived assignment.
std::vector<Base>holding derived objects is almost always a bug.- No GCC warning exists —
-Wall -Wextra -Wpedantic -Wconversionare all silent. - Fix by passing by reference and storing
unique_ptr,shared_ptr, or raw pointers. - Prevent it by making the base abstract — the strongest and cheapest defence.
- Deleted copy operations also block it, but stop derived classes copying their base part.
- Protected copy operations are the usual answer: derived classes can copy, outsiders cannot.
- Abstract base + protected copies + public virtual
clone()is the standard copyable-polymorphic recipe. - Assignment slicing is asymmetric: base-from-derived compiles silently, derived-from-base does not.
Questions about this lesson
Log in to ask a question.