Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 20

Understanding and Preventing Object Slicing

Recommended resources

Hello! Welcome back to our journey into the internals of C++.

In our last lesson, we tackled the critical topic of virtual destructors. We saw how deleting a derived object through a base class pointer can lead to resource leaks if the destructor isn't virtual. The key insight was that delete on a pointer with a non-virtual destructor uses static binding, calling ~Base(), whereas a virtual destructor enables dynamic binding via the vtable, ensuring the correct ~Derived() is called first.

Today, we're going to investigate another classic pitfall of polymorphism: object slicing. This occurs when you treat a derived object as a base object by value instead of by pointer or reference. Our goal is to analyze and prevent object slicing when passing derived classes by value. We will explore what slicing is, why it happens at a memory and vtable level, and how to design our code to avoid it.

1. What is Object Slicing?

Object slicing is the process where a derived class object is copied, assigned, or passed to a base class object, and in the process, it loses its derived-class-specific parts. Only the "base class" portion of the object survives the copy.

Imagine a Derived object's memory layout. It contains the data members of its Base class, plus its own additional data members. The Derived object is literally larger than a Base object.

Visual Explanation of Object Slicing
This diagram shows a Derived class object (totaling 30 KB) being copied into a Base class object, which only has space for the Base portion (10 KB). The Derived-specific data (20 KB) is "sliced" off and does not fit.

When you try to copy this larger Derived object into a variable of type Base, there simply isn't enough room. The compiler resolves this by "slicing" off the derived parts and only copying the base subobject. More importantly, this also strips away the polymorphic behavior of the object.

Object slicing typically occurs in three common scenarios:

  1. Direct assignment of a derived object to a base object.
  2. Passing a derived object by value to a function that expects a base object.
  3. Storing derived objects in an STL container of base objects (e.g., std::vector<Base>).

To see these scenarios in action, let's watch a video that demonstrates the problem and its solution.

C++ - Slicing

The video 'C++ - Slicing' by Coding Jesus provides a clear and concise explanation of what object slicing is and shows the three common ways it can occur in your code.

Please watch the video from 00:59 to 09:19. Pay close attention to: The initial definition and example of slicing with direct assignment (b = d). How passing a derived object by value to a function causes slicing. The issue with storing derived objects in a std::vector<Base>. The consistent solution presented for all cases: using pointers or references.

As the video demonstrated, not only is data from the derived class lost, but virtual function calls no longer resolve to the derived class's implementation. This effectively breaks polymorphism.


2. The Mechanism: Why Slicing Happens

To understand why slicing happens, we need to connect it to two concepts you're familiar with: object memory layout and copy construction.

When you write code like this:

Derived d;
Base b = d; // Slicing occurs here!

The line Base b = d; is a variable initialization. The compiler must create a Base object named b. To do this, it invokes the copy constructor of the Base class: Base::Base(const Base& other).

The Derived object d is implicitly upcast to a const Base& to be passed as an argument to this copy constructor. The Base copy constructor then proceeds to do its job: it copies all the members from the Base part of d into the newly created b. It has no knowledge of any members that exist only in Derived.

Most critically for your interest in internals, the Base copy constructor also initializes the vptr of the new object b. Which vtable do you think it points it to? The Base vtable, of course!

The article below explains this process clearly.

C++ Tutorial: Object Slicing and Virtual Table

The article 'C++ Tutorial: Object Slicing and Virtual Table' on bogotobogo.com provides a great explanation of what happens at the memory and vtable level during slicing.

Please read the section that begins with 'What happened?'. It provides a detailed breakdown of how passing by value causes the compiler to copy only the Animal portion of the Bird object and, crucially, how the Base copy constructor initializes the vptr to the Base vtbl.

So, object slicing isn't a mysterious compiler feature; it's a direct consequence of how copy construction works. When you copy-construct a Base object, you get a Base object, complete with a Base vtable pointer. Any "memory" of it ever being a Derived object is gone.

Let's confirm this with a short experiment. The following code from a Stack Overflow answer clearly shows which copy constructor is called during slicing.

What is object slicing?

This Stack Overflow answer provides a perfect, minimal example to demonstrate which constructor is invoked during slicing.

Please review the code example and its output in this answer. Notice that 'Call by copy' (h(B())) invokes the 'A' copy constructor, not the 'B' one. This is the slicing in action.


3. Preventing Object Slicing

Since slicing happens when objects are copied by value, the solution is to avoid copying them. To preserve polymorphism, you must handle derived objects through base class pointers or references.

Let's revisit the three slicing scenarios and their solutions:

  1. Direct Assignment:

    • Problem: Base b = d;
    • Solution: Use a reference or pointer to refer to the original object.
      Derived d;
      Base& b_ref = d; // No slicing, b_ref refers to d
      Base* b_ptr = &d; // No slicing, b_ptr points to d
      b_ref.whoami();  // Calls Derived::whoami()
      b_ptr->whoami(); // Calls Derived::whoami()
      
  2. Passing by Value to a Function:

    • Problem: void my_func(Base b);
    • Solution: Change the function to accept a reference (preferably const if it doesn't modify the object) or a pointer.
      // Pass by const reference to avoid slicing and an unnecessary copy
      void my_func(const Base& b); 
      
  3. Storing in a Container of Objects:

    • Problem: std::vector<Base> objects; objects.push_back(d);
    • Solution: Store pointers to the objects instead of the objects themselves.
      std::vector<Base*> object_ptrs;
      object_ptrs.push_back(new Derived());
      // ...
      // Don't forget to delete the objects later!
      for (auto ptr : object_ptrs) {
          delete ptr;
      }
      
    • A Better Solution (RAII): Manually managing new and delete is error-prone. A much safer and more modern C++ approach is to use smart pointers, which handle memory automatically. We'll cover these in depth in a future module, but here's a preview:
      #include <memory>
      std::vector<std::unique_ptr<Base>> objects;
      objects.push_back(std::make_unique<Derived>());
      // No manual delete needed!
      
Test your understanding!

You have the following code, which aims to process a list of different Document types.

#include <iostream>
#include <vector>
#include <string>

struct Document {
    virtual std::string get_type() const { return "Generic Document"; }
    virtual ~Document() = default; // Good practice!
};

struct PDF : public Document {
    std::string get_type() const override { return "PDF Document"; }
};

struct Word : public Document {
    std::string get_type() const override { return "Word Document"; }
};

void process_documents(std::vector<Document> docs) {
    for (const auto& doc : docs) {
        std::cout << "Processing: " << doc.get_type() << std::endl;
    }
}

int main() {
    std::vector<Document> documents;
    documents.push_back(PDF());
    documents.push_back(Word());
    process_documents(documents);
    return 0;
}

What will be the output of this program, and why? How would you fix it to produce the intended polymorphic behavior?

Show answer

The output will be:

Processing: Generic Document
Processing: Generic Document

This is because of object slicing. When PDF() and Word() are pushed into std::vector<Document>, they are sliced. Only their Document subobjects are stored in the vector. The process_documents function also accepts the vector by value, causing another copy (and potential slicing, though the objects are already sliced).

To fix this, you must store pointers and pass by reference. The modern C++ way is with std::unique_ptr:

#include <iostream>
#include <vector>
#include <string>
#include <memory> // For std::unique_ptr

// ... (class definitions are the same) ...

// 1. Pass vector by const reference to avoid a copy
void process_documents(const std::vector<std::unique_ptr<Document>>& docs) {
    for (const auto& doc_ptr : docs) {
        // 2. Dereference the pointer to call the virtual function
        std::cout << "Processing: " << doc_ptr->get_type() << std::endl;
    }
}

int main() {
    // 3. Store smart pointers, not objects
    std::vector<std::unique_ptr<Document>> documents;
    documents.push_back(std::make_unique<PDF>());
    documents.push_back(std::make_unique<Word>());
    process_documents(documents);
    return 0;
}

This fixed version will correctly output:

Processing: PDF Document
Processing: Word Document

Conclusion

We've now examined another subtle but critical aspect of polymorphism. While virtual functions provide powerful runtime flexibility, that power is only available when you handle objects through pointers or references.

Key Takeaways:

  • Object slicing happens when a derived object is copied into a base object, losing derived-specific data and behavior.
  • This occurs during direct assignment, passing by value, and insertion into containers of objects.
  • At a low level, slicing is a consequence of the base class's copy constructor being called, which only knows how to copy base members and resets the object's vptr to the base class's vtable.
  • The universal solution is to avoid copying polymorphic objects. Always handle them through pointers or references to preserve their true type and behavior.

In our next lesson, we will continue our deep dive into object memory layout by exploring the complexities introduced by multiple inheritance. We'll see how an object can inherit from more than one base class and what that means for its memory structure, including the possibility of having multiple vptrs.