Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

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 Manager reported 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 -Wconversion are 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.