Object slicing

Let's go back to an example we looked at previously:

#include <iostream>
#include <string_view>

class Entity
{
protected:
    int m_level{};

public:
    Entity(int level)
        : m_level{ level }
    {
    }

    virtual ~Entity() = default;

    virtual std::string_view getType() const { return "Entity"; }
    int getLevel() const { return m_level; }
};

class Enemy: public Entity
{
public:
    Enemy(int level)
        : Entity{ level }
    {
    }

   std::string_view getType() const override { return "Enemy"; }
};

int main()
{
    Enemy goblin{ 8 };
    std::cout << "goblin is a " << goblin.getType() << " and has " << goblin.getLevel() << " levels\n";

    Entity& ref{ goblin };
    std::cout << "ref is a " << ref.getType() << " and has " << ref.getLevel() << " levels\n";

    Entity* ptr{ &goblin };
    std::cout << "ptr is a " << ptr->getType() << " and has " << ptr->getLevel() << " levels\n";

    return 0;
}

In the above example, ref references and ptr points to goblin, which has an Entity part, and an Enemy part. Because ref and ptr are of type Entity, ref and ptr can only see the Entity part of goblin -- the Enemy part of goblin still exists, but simply can't be seen through ref or ptr. However, through use of virtual functions, we can access the most-derived version of a function. Consequently, the above program prints:

goblin is a Enemy and has 8 levels ref is a Enemy and has 8 levels ptr is a Enemy and has 8 levels

But what happens if instead of setting an Entity reference or pointer to an Enemy object, we simply assign an Enemy object to an Entity object?

int main()
{
    Enemy goblin{ 8 };
    Entity entity{ goblin }; // what happens here?
    std::cout << "entity is a " << entity.getType() << " and has " << entity.getLevel() << " levels\n";

    return 0;
}

Remember that goblin has an Entity part and an Enemy part. When we assign an Enemy object to an Entity object, only the Entity portion of the Enemy object is copied. The Enemy portion is not. In the example above, entity receives a copy of the Entity portion of goblin, but not the Enemy portion. That Enemy portion has effectively been "sliced off". Consequently, the assigning of an Enemy class object to an Entity class object is called object slicing (or slicing for short).

Because entity was and still is just an Entity, Entity's virtual pointer still points to Entity. Thus, entity.getType() resolves to Entity::getType().

The above example prints:

entity is a Entity and has 8 levels

Used conscientiously, slicing can be benign. However, used improperly, slicing can cause unexpected results in quite a few different ways. Let's examine some of those cases.

Slicing and functions

Now, you might think the above example is a bit silly. After all, why would you assign goblin to entity like that? You probably wouldn't. However, slicing is much more likely to occur accidentally with functions.

Consider the following function:

void displayInfo(const Entity entity) // note: entity passed by value, not reference
{
    std::cout << "I am a " << entity.getType() << '\n';
}

This is a pretty simple function with a const entity object parameter that is passed by value. If we call this function like such:

int main()
{
    Enemy goblin{ 8 };
    displayInfo(goblin); // oops, didn't realize this was pass by value on the calling end

    return 0;
}

When you wrote this program, you may not have noticed that entity is a value parameter, not a reference. Therefore, when called as displayInfo(goblin), while we might have expected entity.getType() to call virtualized function getType() and print "I am a Enemy", that is not what happens. Instead, Enemy object goblin is sliced and only the Entity portion is copied into the entity parameter. When entity.getType() executes, even though the getType() function is virtualized, there's no Enemy portion of the class for it to resolve to. Consequently, this program prints:

I am a Entity

In this case, it's pretty obvious what happened, but if your functions don't actually print any identifying information like this, tracking down the error can be challenging.

Of course, slicing here can all be easily avoided by making the function parameter a reference instead of a pass by value (yet another reason why passing classes by reference instead of value is a good idea).

void displayInfo(const Entity& entity) // note: entity now passed by reference
{
    std::cout << "I am a " << entity.getType() << '\n';
}

int main()
{
    Enemy goblin{ 8 };
    displayInfo(goblin);

    return 0;
}

This prints:

I am a Enemy

Slicing vectors

Yet another area where new programmers run into trouble with slicing is trying to implement polymorphism with std::vector. Consider the following program:

#include <vector>

int main()
{
	std::vector<Entity> v{};
	v.push_back(Entity{ 4 });    // add an Entity object to our vector
	v.push_back(Enemy{ 8 }); // add an Enemy object to our vector

        // Print out all of the elements in our vector
	for (const auto& element : v)
		std::cout << "I am a " << element.getType() << " with " << element.getLevel() << " levels\n";

	return 0;
}

This program compiles just fine. But when run, it prints:

I am a Entity with 4 levels I am a Entity with 8 levels

Similar to the previous examples, because the std::vector was declared to be a vector of type Entity, when Enemy(8) was added to the vector, it was sliced.

Fixing this is a little more difficult. Many new programmers try creating a std::vector of references to an object, like this:

std::vector<Entity&> v{};

Unfortunately, this won't compile. The elements of std::vector must be assignable, whereas references can't be reassigned (only initialized).

One way to address this is to make a vector of pointers:

#include <iostream>
#include <vector>

int main()
{
	std::vector<Entity*> v{};

	Entity e{ 4 }; // e and g can't be anonymous objects
	Enemy g{ 8 };

	v.push_back(&e); // add an Entity object to our vector
	v.push_back(&g); // add an Enemy object to our vector

	// Print out all of the elements in our vector
	for (const auto* element : v)
		std::cout << "I am a " << element->getType() << " with " << element->getLevel() << " levels\n";

	return 0;
}

This prints:

I am a Entity with 4 levels I am a Enemy with 8 levels

which works! A few comments about this. First, nullptr is now a valid option, which may or may not be desirable. Second, you now have to deal with pointer semantics, which can be awkward. But the upside is that using pointers allows us to put dynamically allocated objects in the vector (just don't forget to explicitly delete them).

Another option is to use std::reference_wrapper, which is a class that mimics an reassignable reference:

#include <functional> // for std::reference_wrapper
#include <iostream>
#include <string_view>
#include <vector>

class Entity
{
protected:
    int m_level{};

public:
    Entity(int level)
        : m_level{ level }
    {
    }
    virtual ~Entity() = default;

    virtual std::string_view getType() const { return "Entity"; }
    int getLevel() const { return m_level; }
};

class Enemy : public Entity
{
public:
    Enemy(int level)
        : Entity{ level }
    {
    }

    std::string_view getType() const override { return "Enemy"; }
};

int main()
{
	std::vector<std::reference_wrapper<Entity>> v{}; // a vector of reassignable references to Entity

	Entity e{ 4 }; // e and g can't be anonymous objects
	Enemy g{ 8 };

	v.push_back(e); // add an Entity object to our vector
	v.push_back(g); // add an Enemy object to our vector

	// Print out all of the elements in our vector
	// we use .get() to get our element out of the std::reference_wrapper
	for (const auto& element : v) // element has type const std::reference_wrapper<Entity>&
		std::cout << "I am a " << element.get().getType() << " with " << element.get().getLevel() << " levels\n";

	return 0;
}

The Frankenobject

In the above examples, we've seen cases where slicing lead to the wrong result because the derived class had been sliced off. Now let's take a look at another dangerous case where the derived object still exists!

Consider the following code:

int main()
{
    Enemy g1{ 4 };
    Enemy g2{ 8 };
    Entity& e{ g2 };

    e = g1; // this line is problematic

    return 0;
}

The first three lines in the function are pretty straightforward. Create two Enemy objects, and set an Entity reference to the second one.

The fourth line is where things go astray. Since e points at g2, and we're assigning g1 to e, you might think that the result would be that g1 would get copied into g2 -- and it would, if e were an Enemy. But e is an Entity, and the operator= that C++ provides for classes isn't virtual by default. Consequently, the assignment operator that copies an Entity is invoked, and only the Entity portion of g1 is copied into g2.

As a result, you'll discover that g2 now has the Entity portion of g1 and the Enemy portion of g2. In this particular example, that's not a problem (because the Enemy class has no data of its own), but in most cases, you'll have just created a Frankenobject -- composed of parts of multiple objects.

Worse, there's no easy way to prevent this from happening (other than avoiding assignments like this as much as possible).

Best practice
If the base class is not designed to be instantiated by itself (e.g. it is just an interface class), slicing can be avoided by making the base class non-copyable (by deleting the copy constructor and assignment operator).

Conclusion

Although C++ supports assigning derived objects to base objects via object slicing, in general, this is likely to cause nothing but headaches, and you should generally try to avoid slicing. Make sure your function parameters are references (or pointers) and try to avoid any kind of pass-by-value when it comes to derived classes.

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 - the derived portion is "sliced off". The result is a base class object that has lost all derived class specific data and behavior.

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.

Slicing with vectors: When creating a std::vector of base class objects, adding derived class objects to the vector will slice them. The vector stores only the base class portions. Solutions include using a vector of pointers (std::vector<Base*>) or using std::reference_wrapper (std::vector<std::reference_wrapper<Base>>).

The Frankenobject: When assigning a derived object to another derived object through a base class reference or pointer, only the base class portion is copied, potentially creating an object with the base part from one object and the derived part from another. This is dangerous and should be avoided.

Preventing slicing: To avoid 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.

Object slicing is a common source of bugs in C++ when working with inheritance. Understanding when and how it occurs, and consistently using references or pointers for polymorphic types, helps prevent these issues. The key takeaway is to avoid pass-by-value with derived classes and prefer references or pointers for polymorphic behavior.