What Is Object Slicing?

Object slicing is what happens when a derived object is copied into a base object. The destination is only as large as the base class, so the derived part of the original never travels with the copy: it is sliced off. What survives is an ordinary base object, and every virtual call made on it resolves to the base version.

The distinction that matters here is between naming a derived object through a base type and copying one into a base type. A base reference or a base pointer only names the object. The derived part is still sitting in memory where it always was, invisible through that handle but reachable by virtual dispatch. A base object is a different object with its own storage, and that storage has no room for the derived part.

Here are both situations in one program. Alert is the base, PagerAlert adds a retry budget and a hold window of its own:

#include <iostream>
#include <string_view>

class Alert
{
protected:
    int m_code{};

public:
    explicit Alert(int code)
        : m_code{ code }
    {
    }

    virtual ~Alert() = default;

    virtual std::string_view channel() const { return "console"; }

    int getCode() const { return m_code; }
};

class PagerAlert : public Alert
{
private:
    int m_retries{};
    int m_holdMinutes{};

public:
    PagerAlert(int code, int retries, int holdMinutes)
        : Alert{ code }
        , m_retries{ retries }
        , m_holdMinutes{ holdMinutes }
    {
    }

    std::string_view channel() const override { return "pager"; }

    int getRetries() const { return m_retries; }
    int getHoldMinutes() const { return m_holdMinutes; }
};

int main()
{
    PagerAlert onCall{ 4021, 3, 15 };

    std::cout << "sizeof(Alert) is " << sizeof(Alert) << " bytes" << '\n';
    std::cout << "sizeof(PagerAlert) is " << sizeof(PagerAlert) << " bytes" << '\n';

    Alert& handle{ onCall };
    Alert* address{ &onCall };
    Alert copy{ onCall };

    std::cout << "object    -> " << onCall.channel() << '\n';
    std::cout << "reference -> " << handle.channel() << '\n';
    std::cout << "pointer   -> " << address->channel() << '\n';
    std::cout << "copy      -> " << copy.channel() << '\n';

    return 0;
}
sizeof(Alert) is 16 bytes
sizeof(PagerAlert) is 24 bytes
object    -> pager
reference -> pager
pointer   -> pager
copy      -> console

handle and address are three different ways of pointing at the same 24 bytes, and all three reach PagerAlert::channel(). copy is not another way of pointing at onCall at all. It is a separate 16-byte object built out of the base half of onCall, and it answers as a plain Alert because that is exactly what it is.

Why The Copy Cannot Keep The Derived Part

Nothing subtle is going on inside Alert copy{ onCall };. Three ordinary rules combine:

  1. copy is a variable of type Alert, so the compiler reserves sizeof(Alert) bytes for it. The eight extra bytes that PagerAlert needs were never allocated, and no copy operation can invent them.
  2. Overload resolution looks for a constructor of Alert that accepts a PagerAlert. The only candidate is the implicitly generated copy constructor Alert(const Alert&). A PagerAlert binds to that base reference happily, and the constructor copies the one member it knows about: m_code.
  3. The virtual pointer inside copy is installed by Alert's own constructor, not copied from the source. That is what makes copy.channel() a call to Alert::channel().
Key Mechanism
Slicing is not a failure of virtual dispatch. Dispatch does its job perfectly. It is simply being asked about an object that genuinely is an Alert and nothing more, because that is what the copy produced.

Where Slicing Gets In

Because the trigger is a copy into a base-typed destination, you can find every place slicing can occur by asking one question of your code: does a base object get built or assigned here? There are five common answers.

Copy site What it looks like Result
Initializing a base object Alert copy{ onCall }; derived part dropped
A by-value parameter void routeByValue(Alert alert) derived part dropped at the call
Returning a base by value Alert latest() { return onCall; } derived part dropped on return
Inserting into a container of base values board.push_back(onCall); derived part dropped per element
Assigning through a base reference or pointer handle = diskAlert; destination keeps its own derived part

The first entry is the easy one to spot, and also the one you are least likely to write by accident. The other four are where real bugs come from, so the rest of this lesson works through them.

A Value Parameter Is A Copy

A function parameter declared by value is initialized by copying the argument, which puts a full slicing event at every call site. Nothing at the call looks unusual, which is what makes this the most common way to hit the problem:

#include <iostream>
#include <string_view>

class Alert
{
protected:
    int m_code{};

public:
    explicit Alert(int code)
        : m_code{ code }
    {
    }

    virtual ~Alert() = default;

    virtual std::string_view channel() const { return "console"; }

    int getCode() const { return m_code; }
};

class PagerAlert : public Alert
{
private:
    int m_retries{};
    int m_holdMinutes{};

public:
    PagerAlert(int code, int retries, int holdMinutes)
        : Alert{ code }
        , m_retries{ retries }
        , m_holdMinutes{ holdMinutes }
    {
    }

    std::string_view channel() const override { return "pager"; }

    int getRetries() const { return m_retries; }
    int getHoldMinutes() const { return m_holdMinutes; }
};

void routeByValue(Alert alert)
{
    std::cout << "as a copy:      code " << alert.getCode() << " goes to " << alert.channel() << '\n';
}

void routeByReference(const Alert& alert)
{
    std::cout << "as a reference: code " << alert.getCode() << " goes to " << alert.channel() << '\n';
}

int main()
{
    PagerAlert onCall{ 4021, 3, 15 };

    routeByValue(onCall);
    routeByReference(onCall);

    return 0;
}
as a copy:      code 4021 goes to console
as a reference: code 4021 goes to pager

Both functions have the same body. The only difference is a single & in the parameter list, and it decides whether the alert reaches the on-call engineer or quietly goes to a log file. Notice too that the numeric data survived in both cases, so a program that only prints getCode() looks completely healthy while its behaviour is wrong.

Best Practice
Pass polymorphic types by reference or by pointer, never by value. If you want to be sure a parameter is never sliced, write const Alert& and let the caller keep ownership of the object.

A Container Of Base Objects Slices Everything Put Into It

A std::vector<Alert> stores Alert objects, laid out end to end, 16 bytes each. Inserting a PagerAlert therefore has to copy it into an Alert-sized slot, which is the same event as before:

#include <iostream>
#include <string_view>
#include <vector>

class Alert
{
protected:
    int m_code{};

public:
    explicit Alert(int code)
        : m_code{ code }
    {
    }

    virtual ~Alert() = default;

    virtual std::string_view channel() const { return "console"; }

    int getCode() const { return m_code; }
};

class PagerAlert : public Alert
{
private:
    int m_retries{};
    int m_holdMinutes{};

public:
    PagerAlert(int code, int retries, int holdMinutes)
        : Alert{ code }
        , m_retries{ retries }
        , m_holdMinutes{ holdMinutes }
    {
    }

    std::string_view channel() const override { return "pager"; }

    int getRetries() const { return m_retries; }
    int getHoldMinutes() const { return m_holdMinutes; }
};

int main()
{
    std::vector<Alert> board{};
    board.push_back(Alert{ 3110 });
    board.push_back(PagerAlert{ 4021, 3, 15 });

    for (const auto& entry : board)
        std::cout << "code " << entry.getCode() << " goes to " << entry.channel() << '\n';

    return 0;
}
code 3110 goes to console
code 4021 goes to console

The program compiles without a single warning, which is worth pausing on: the compiler is doing precisely what the type of the container asked for. The vector was declared to hold Alert values, and it holds Alert values.

The instinctive fix is to store references instead, so that nothing is copied. That does not compile:

std::vector<Alert&> board{};

A vector needs to be able to allocate storage for its elements, take their addresses, and assign one element over another when it grows or when you erase from the middle. A reference supports none of that: it can be bound once at initialization and can never be reseated afterwards, and there is no such thing as a pointer to a reference. So the element type of a standard container has to be a real object type.

Laid side by side, the four candidate element types come out like this, and only the bottom two are workable:

Element type Compiles? Keeps the derived part? Cost
std::vector<Alert> yes no, every insertion slices silent wrong behaviour
std::vector<Alert&> no not applicable not usable
std::vector<Alert*> yes yes, nothing is copied nullptr becomes possible, and you manage lifetimes
std::vector<std::reference_wrapper<Alert>> yes yes, nothing is copied every access goes through .get()

std::reference_wrapper is a small class template that holds a pointer internally while behaving like a reference you are allowed to reassign, which is exactly the gap that stopped std::vector<Alert&> from working. Here are both containers over the same two objects:

#include <functional>
#include <iostream>
#include <string_view>
#include <vector>

class Alert
{
protected:
    int m_code{};

public:
    explicit Alert(int code)
        : m_code{ code }
    {
    }

    virtual ~Alert() = default;

    virtual std::string_view channel() const { return "console"; }

    int getCode() const { return m_code; }
};

class PagerAlert : public Alert
{
private:
    int m_retries{};
    int m_holdMinutes{};

public:
    PagerAlert(int code, int retries, int holdMinutes)
        : Alert{ code }
        , m_retries{ retries }
        , m_holdMinutes{ holdMinutes }
    {
    }

    std::string_view channel() const override { return "pager"; }

    int getRetries() const { return m_retries; }
    int getHoldMinutes() const { return m_holdMinutes; }
};

int main()
{
    Alert nightly{ 3110 };
    PagerAlert onCall{ 4021, 3, 15 };

    std::vector<Alert*> byPointer{ &nightly, &onCall };
    std::vector<std::reference_wrapper<Alert>> byWrapper{ nightly, onCall };

    for (const auto* entry : byPointer)
        std::cout << "pointer: code " << entry->getCode() << " goes to " << entry->channel() << '\n';

    for (const auto& entry : byWrapper)
        std::cout << "wrapper: code " << entry.get().getCode() << " goes to " << entry.get().channel() << '\n';

    return 0;
}
pointer: code 3110 goes to console
pointer: code 4021 goes to pager
wrapper: code 3110 goes to console
wrapper: code 4021 goes to pager

Both containers hold indirections rather than objects, so the two alerts stay exactly where main() created them at full size and virtual dispatch keeps working. Note that nightly and onCall have to be named variables: a temporary would be destroyed at the end of the statement, leaving the container full of dangling entries.

Watch The Lifetimes
Neither a pointer nor a std::reference_wrapper owns what it refers to. The referenced objects must outlive the container. When the container has to own its elements, reach for smart pointers instead of raw pointers.

Assignment Through A Base Reference Builds A Frankenobject

Every case so far ended with the derived part missing. This last one is nastier, because the derived part is still present and is now attached to the wrong data.

Assignment is a member function like any other, and the compiler-generated operator= is not virtual. When the left-hand side of an assignment is a base reference, the base class assignment operator is the one that runs, whatever the objects underneath actually are:

#include <iostream>
#include <string_view>

class Alert
{
protected:
    int m_code{};

public:
    explicit Alert(int code)
        : m_code{ code }
    {
    }

    virtual ~Alert() = default;

    virtual std::string_view channel() const { return "console"; }

    int getCode() const { return m_code; }
};

class PagerAlert : public Alert
{
private:
    int m_retries{};
    int m_holdMinutes{};

public:
    PagerAlert(int code, int retries, int holdMinutes)
        : Alert{ code }
        , m_retries{ retries }
        , m_holdMinutes{ holdMinutes }
    {
    }

    std::string_view channel() const override { return "pager"; }

    int getRetries() const { return m_retries; }
    int getHoldMinutes() const { return m_holdMinutes; }
};

int main()
{
    PagerAlert diskAlert{ 4021, 3, 15 };
    PagerAlert linkAlert{ 5240, 9, 2 };

    Alert& handle{ linkAlert };
    handle = diskAlert;

    std::cout << "linkAlert code:    " << linkAlert.getCode() << '\n';
    std::cout << "linkAlert retries: " << linkAlert.getRetries() << '\n';
    std::cout << "linkAlert hold:    " << linkAlert.getHoldMinutes() << '\n';

    return 0;
}
linkAlert code:    4021
linkAlert retries: 9
linkAlert hold:    2

Both operands were PagerAlert objects, so it would be reasonable to expect linkAlert to become a full copy of diskAlert. It did not. Alert::operator= ran, copied m_code across, and left m_retries and m_holdMinutes untouched. linkAlert now carries the identity of the disk alert and the retry policy of the link alert: an object stitched together from two sources, which is why this result is nicknamed a Frankenobject.

Danger
A Frankenobject is worse than a plain slice because nothing is obviously missing. The object still has a complete set of members, they simply describe two different things at once, and every later invariant check you wrote will happily pass.

There is no keyword that switches this behaviour off. Assignment through a base handle is legal C++, so the defence has to come from the design of the base class itself.

Making Slicing Impossible By Design

If a base class exists only to define an interface and was never meant to be instantiated on its own, then no correct program needs to copy one. Say so in the class, by deleting the copy constructor and the copy assignment operator:

class Alert
{
protected:
    int m_code{};

public:
    explicit Alert(int code)
        : m_code{ code }
    {
    }

    Alert(const Alert&) = delete;
    Alert& operator=(const Alert&) = delete;

    virtual ~Alert() = default;

    virtual std::string_view channel() const { return "console"; }

    int getCode() const { return m_code; }
};

Now every copy site listed earlier stops compiling. Writing Alert copy{ onCall }; reports use of deleted function, a by-value parameter of type Alert is rejected at the call, std::vector<Alert> cannot insert, and the Frankenobject assignment is refused because operator= is gone. Slicing has been converted from a silent runtime surprise into a compiler error, which is the best trade in the language.

Best Practice
Make a base class non-copyable when it is not meant to be instantiated by itself. Pass and store polymorphic objects through references or pointers, and treat any base-typed variable, parameter, or container element as a decision you made deliberately rather than one you drifted into.

Summary

Object slicing: Object slicing occurs when a derived class object is assigned to a base class object. Only the base class portion of the derived object is copied and the derived portion is sliced off, leaving a base class object that has lost the derived data and the derived behaviour. Naming a derived object through a base reference or pointer does not slice it, because no copy is made.

Why it happens: A base-typed variable is only sizeof(Base) bytes, overload resolution selects the base class copy constructor, and the destination's virtual pointer is installed by the base constructor. Virtual dispatch then correctly reports a base object.

Slicing with functions: Object slicing commonly occurs accidentally with functions when a parameter is passed by value instead of by reference. If a function accepts a base class parameter by value and is passed a derived class object, the derived portion is sliced off during the copy. Returning a base class by value slices in the same way.

Slicing with vectors: When creating a std::vector of base class objects, adding derived class objects to the vector will slice them, because the vector stores base-sized elements. A std::vector<Base&> does not compile, since container elements must be assignable objects and a reference can never be reseated. Solutions include a vector of pointers (std::vector<Base*>) or std::vector<std::reference_wrapper<Base>>, both of which store an indirection instead of a copy.

The Frankenobject: Assigning one derived object to another through a base class reference or pointer runs the non-virtual base class operator=, so only the base portion is copied. The target ends up with the base part from one object and the derived part from another, an inconsistent state that no member is missing from.

Preventing slicing: Use references or pointers instead of passing objects by value. If a base class should not be instantiated directly, make it non-copyable by deleting the copy constructor and copy assignment operator, which turns every slicing site into a compile error.

Slicing is caused by copying, not by inheritance, so the habit that prevents it is simple: whenever a base-typed object, parameter, or container element appears in your code, check that you meant to create a base object rather than to refer to a derived one.