What Is Object Slicing

Object Slicing, Is it advantage?

In C++, you should think of an object slice as a conversion from the derived type to the base type[*]. A brand new object is created, which is "inspired by a true story".

Sometimes this is something that you would want to do, but the result is not in any sense the same object as the original. When object slicing goes wrong is when people aren't paying attention, and think it is the same object or a copy of it.

It's normally not beneficial. In fact it's normally done accidentally when someone passes by value when they meant to pass by reference.

It's quite hard to come up with an example of when slicing is definitively the right thing to do, because it's quite hard (especially in C++) to come up with an example where a non-abstract base class is definitively the right thing to do. This is an important design point, and not one to pass over lightly - if you find yourself slicing an object, either deliberately or accidentally, quite likely your object hierarchy is wrong to start with. Either the base class shouldn't be used as a base class, or else it should have at least one pure virtual function and hence not be sliceable or passable by value.

So, any example I gave where an object is converted to an object of its base class, would rightly provoke the objection, "hang on a minute, what are you doing inheriting from a concrete class in the first place?". If slicing is accidental then it's probably a bug, and if it's deliberate then it's probably "code smell".

But the answer might be "yes, OK, this shouldn't really be how things are structured, but given that they are structured that way, I need to convert from the derived class to the base class, and that by definition is a slice". In that spirit, here's an example:

struct Soldier {
string name;
string rank;
string serialNumber;
};

struct ActiveSoldier : Soldier {
string currentUnit;
ActiveSoldier *commandingOfficer; // the design errors multiply!
int yearsService;
};

template <typename InputIterator>
void takePrisoners(InputIterator first, InputIterator last) {
while (first != last) {
Soldier s(*first);
// do some stuff with name, rank and serialNumber
++first;
}
}

Now, the requirement of the takePrisoners function template is that its parameter be an iterator for a type convertible to Soldier. It doesn't have to be a derived class, and we don't directly access the members "name", etc, so takePrisoners has tried to offer the easiest possible interface to implement given the restrictions (a) should work with Soldier, and (b) should be possible to write other types that it also works with.

ActiveSoldier is one such other type. For reasons best known only to the author of that class, it has opted to publicly inherit from Soldier rather than providing an overloaded conversion operator. We can argue whether that's ever a good idea, but let's suppose we're stuck with it. Because it's a derived class, it is convertible to Soldier. That conversion is called a slice. Hence, if we call takePrisoners passing in the begin() and end() iterators for a vector of ActiveSoldiers, then we will slice them.

You could probably come up with similar examples for an OutputIterator, where the recipient only cares about the base class part of the objects being delivered, and so allows them to be sliced as they're written to the iterator.

The reason it's "code smell" is that we should consider (a) rewriting ActiveSoldier, and (b) changing Soldier so that it can be accessed using functions instead of member access, so that we can abstract that set of functions as an interface that other types can implement independently, so that takePrisoners doesn't have to convert to Soldier. Either of those would remove the need for a slice, and would have potential benefits for the ease with which our code can be extended in future.

[*] because it is one. The last two lines below are doing the same thing:

struct A {
int value;
A(int v) : value(v) {}
};

struct B : A {
int quantity;
B(int v, int q) : A(v), quantity(q) {}
};

int main() {
int i = 12; // an integer
B b(12, 3); // an instance of B
A a1 = b; // (1) convert B to A, also known as "slicing"
A a2 = i; // (2) convert int to A, not known as "slicing"
}

The only difference is that (1) calls A's copy constructor (that the compiler provides even though the code doesn't), whereas (2) calls A's int constructor.

As someone else said, Java doesn't do object slicing. If the code you provide were turned into Java, then no kind of object slicing would happen. Java variables are references, not objects, so the postcondition of a = b is just that the variable "a" refers to the same object as the variable "b" - changes via one reference can be seen via the other reference, and so on. They just refer to it by a different type, which is part of polymorphism. A typical analogy for this is that I might think of a person as "my brother"[**], and someone else might think of the same person as "my vicar". Same object, different interface.

You can get the Java-like effect in C++ using pointers or references:

B b(24,7);
A *a3 = &b; // No slicing - a3 is a pointer to the object b
A &a4 = b; // No slicing - a4 is a reference to (pseudonym for) the object b

[**] In point of fact, my brother is not a vicar.

Avoiding object slicing

The fundamental issue is copying an object (which is not an issue in languages where classes are "reference types", but in C++ the default is to pass things by value, i.e. making a copy). "Slicing" means copying the value of a bigger object (of type B, which derives from A) into a smaller object (of type A). Because A is smaller, only a partial copy is made.

When you create a container, its elements are full objects of their own. For example:

std::vector<int> v(3);  // define a vector of 3 integers
int i = 42;
v[0] = i; // copy 42 into v[0]

v[0] is an int variable, just like i.

The same thing happens with classes:

class Base { ... };
std::vector<Base> v(3); // instantiates 3 Base objects
Base x(42);
v[0] = x;

The last line copies the contents of the x object into the v[0] object.

If we change the type of x like this:

class Derived : public Base { ... };
std::vector<Base> v(3);
Derived x(42, "hello");
v[0] = x;

... then v[0] = x tries to copy the contents of a Derived object into a Base object. What happens in this case is that all members declared in Derived are ignored. Only the data members declared in the base class Base are copied, because that's all v[0] has room for.

What a pointer gives you is the ability to avoid copying. When you do

T x;
T *ptr = &x;

, ptr is not a copy of x, it just points to x.

Similarly, you can do

Derived obj;
Base *ptr = &obj;

&obj and ptr have different types (Derived * and Base *, respectively), but C++ allows this code anyway. Because Derived objects contain all members of Base, it's OK to let a Base pointer point at a Derived instance.

What this gives you is essentially a reduced interface to obj. When accessed through ptr, it only has the methods declared in Base. But because no copying was done, all data (including the Derived specific parts) are still there and can be used internally.

As for virtual: Normally, when you call a method foo through an object of type Base, it will invoke exactly Base::foo (i.e. the method defined in Base). This happens even if the call is made through a pointer that actually points at a derived object (as described above) with a different implementation of the method:

class Base {
public:
void foo() const { std::cout << "hello from Base::foo\n"; }
};

class Derived : public Base {
public:
void foo() const { std::cout << "hello from Derived::foo\n"; }
};

Derived obj;
Base *ptr = &obj;
obj.foo(); // calls Derived::foo
ptr->foo(); // calls Base::foo, even though ptr actually points to a Derived object

By marking foo as virtual, we force the method call to use the actual type of the object, instead of the declared type of the pointer the call is made through:

class Base {
public:
virtual void foo() const { std::cout << "hello from Base::foo\n"; }
};

class Derived : public Base {
public:
void foo() const { std::cout << "hello from Derived::foo\n"; }
};

Derived obj;
Base *ptr = &obj;
obj.foo(); // calls Derived::foo
ptr->foo(); // also calls Derived::foo

virtual has no effect on normal objects because there the declared type and the actual type are always the same. It only affects method calls made through pointers (and references) to objects, because those have the ability to refer to other objects (of potentially different types).

And that is another reason to store a collection of pointers: When you have several different subclasses of GameObject, all of which implement their own custom draw method, you want the code to pay attention to the actual types of the objects, so the right method gets called in each case. If draw weren't virtual, your code would attempt to invoke GameObject::draw, which doesn't exist. Depending on how exactly you code it, this either wouldn't compile in the first place or abort at runtime.

Virtual Table and Object Slicing

The vptr is NOT copied. Let's try to reason about why.

Looking at your main function:

Derived objD;
Base objB;
objB = objD; // <-- HERE
objB.Display();

In line 3, you are assigning objD to objB. This is actually calling Base's assignment operator (which is automatically defined):

Base& operator=(const Base& other)

and it is being passed objD as a Base&. So, your question becomes, "Does the assignment operator copy the vptr"? The answer is, "no". The default assignment operator only copies fields on the class.

You may then ask, "Why wouldn't it copy the vptr too?" The reason is that, if it copied the vptr, methods on the Base class would end up using methods implemented on the Derived class. However, in full generality, those methods could use data members that only exists on the Derived class (and that don't exist on the Base class). Calling those methods would therefore be nonsensical (the data logically doesn't exist on the Base class), and so the language rightly chooses not to do this.

The main issue is that, when you assign the Derived class to the Base class, the variable you're assigning to only holds the fields for the Base class, so the fields in the Derived class that aren't in the Base class are not copied. Therefore, methods from the Derived class won't make sense when called on the Base class.

Note that this isn't the case if, instead, you were to assign a Base pointer or a Base reference to the Derived class. In that case, the original Derived class instance still exists. It's sitting in memory somewhere and has all the Base+Derived class fields. Therefore, methods for the Derived class called on that instance will have access to those fields, and so being able to call those methods still makes sense.

This is why, in C++, to do polymorphism (via virtual methods), you need to use a reference or pointer. See here for a similar discussion:
Why doesn't polymorphism work without pointers/references?

What does slicing mean in C++?

Quoting this lecture:

Slicing


Suppose that class D is derived from
class C. We can think of D as class C
with some extra data and methods. In
terms of data, D has all the data that
C has, and possible more. In terms of
methods, D cannot hide any methods of
C, and may have additional methods. In
terms of existing methods of C, the
only thing that D can do is to
override them with its own versions.

If x is an object of class D, then we
can slice x with respect to C, by
throwing away all of the extensions
that made x a D, and keeping only the
C part. The result of the slicing is
always an object of class C.

slicing http://webdocs.cs.ualberta.ca/~hoover/Courses/201/201-New-Notes/lectures/slides/slice/slide1.gif

Design Principle: Slicing an object
with respect to a parent class C
should still produce a well-formed
object of class C.

Usage Warning: Even though D is-a C,
you must be careful. If you have a
argument type that is a C and you
supply a D it will be sliced if you
are doing call by value, pointer, or
reference. See the example below.

Note on virtual functions. Their
signatures are used to identify which
one to execute.

Watch out for the sliced = operator,
it can make the lhs inconsistent.
Also, the operator= is never virtual,
it wouldn't make sense. For example,
suppose classes A, B are both
subclasses of class C. Just because an
A is a C, and a B is a C, it doesn't
mean you can assign a B object to an A
object. Without run-time type
information you cannot make a safe
assignment.

Is object slicing ever useful?

Sure, it can be useful when wanting to drop the derived portion of class, perhaps to drop dependencies.

For example say we have an object system setup where each base belongs to a derived type, and each derived type has various dependencies, perhaps fulfilled through dependency injection. A clone of a base might want to be created, though a completely new set of dependencies for the actual derived type of that base may want to be assigned.

This can be likened to an game engine where there are many types of colliders. Each collider derives from a base interface-like object in various ways. We want to clone a collider to retrieve it's position and scale (from base), but want to place an entirely different derived implementation on-top of this base. "Object slicing" could be a simple way to achieve this.

In reality a component, or aggregate object organization would make a lot more sense than object slicing specifically, but it's mostly the same idea.

Polymorphic Vectors Without Object Slicing [C++]

Obviously the derived objects have been sliced.

Actually, no. They are not being sliced. You are doing the correct thing in storing base-class pointers to derived-class objects in your array.

No, the code doesn't work the way you want, not because of object slicing, but because you simply did not mark hello() as virtual in Base and as override in the derived classes. So, when you call i->hello() inside your loop, there is no polymorphic dispatch being performed.

Also, you are using std::make_unique() incorrectly. The parameters of make_unique<T>() are passed as-is to the constructor of T. In this case, you are copy-constructing your Child1/Child2 classes from temporary Child1/Child2 objects, which is not necessary. You can get rid of the temporaries.

You are also lacking a virtual destructor in Base. Which will cause undefined behavior when your array goes out of scope and the unique_ptrs try to call delete on their Base* pointers. Only the Base destructor will be called, but not the Child1/Child2 destructors.

Try this instead:

#include <iostream>
#include <array>
#include <memory>

class Base {
public:
virtual ~Base() = default;

virtual void hello() {
std::cout << "This shouldn't print\n";
}
};

class Child1 : public Base {
public:
void hello() override {
std::cout << "Hi from child 1!\n";
}
};

class Child2 : public Base {
public:
void hello() override {
std::cout << "Hi from child 2!\n";
}
};

int main() {
std::array<std::unique_ptr<Base>, 2> array = {
std::make_unique<Child1>(),
std::make_unique<Child2>()
};

for (auto &i : array) {
i->hello();
}
}

Online Demo



Related Topics



Leave a reply



Submit