Suppose you're writing code to represent a zoo. Each animal has its specificities, of course, which can be self-contained in their class. There are also common behaviors between certain animals: both a cat and a tiger are felines, purr, jump, scratch, have claws...
Of course, you know about [[Inheritance|inheritance]], so you've defined a class `Feline` to implement those common behaviors, and defined derived classes `Tiger` and `Cat`, encapsulating their own specific traits.
Now, the zoo also has its organization, and while some of its processes are animal-dependent, some are applied regardless of species.
All animals live in the zoo, so it would make sense to keep them all together, so we can find them and their information more easily. One approach might be to try and create an `std::vector` for each and every type of `Animal` you might have.
You'll have to remember to loop over each type's `std::vector` container in each `Zoo` member method that applies to an `Animal`, regardless of its type. This creates exponential complexity and is hardly maintainable ! At one point or another, you'll make mistakes.
An ideal solution would be to be able to keep all animals in a single container, and to be able to implement a single `Animal` loop in each cross-species `Zoo` member method.
You could then add indefinitely many types of animals to the zoo without having to worry about changing a single method. This would agree well with encapsulation principles.
Let us call such heterogenous containers and functions *polymorphic*.
There's a catch, of course. Two things are preventing us from implementing things the way we'd like: **object slicing** and **static dispatch**.
In the following sections, we'll see how using **pointers and references** solves object slicing, and how **virtual functions** solve the problem of static dispatch, allowing for true polymorphic behavior.
## Object Slicing
Object slicing is what happens when you assign an object to a variable of one of its parents' type (the only situation where that could work). The assigned object loses any additional members he might have had compared to its parent.
For now, there are only felines in the zoo, so we'll start with that.
Below is an illustration of its process. A `Cat` is fit into a `Feline`. By this process, the cat is stripped of everything that makes it a `Cat`, while retaining all that makes him a `Feline`.
```cpp
class Feline
{
public:
void purr()
{
std::cout << "Purrrrr..." << std::endl;
}
};
class Cat : public Feline
{
public:
void meow()
{
std::cout << "Meeeoooooow..." << std::endl;
}
};
int main()
{
Feline f;
Cat c;
/*
Object slicing happens here
We strip the c of what makes it a Cat
so that it's left with what makes it a Feline
*/
f = c;
/*
✅ Works, outputs "Purrrrr..." to the console
f is of type Feline, and feline have a member purr()
*/
f.purr();
/*
🚫 The following yields a compile time error
The (static) type of f is Feline, which has no member meow().
*/
f.meow();
return 0;
}
```
From this, we can infer that storing heterogenous types into an `std::vector` will, in fact, not store the objects we intend but sliced version of them.
```cpp
int main()
{
std::vector<Feline> zoo;
Cat c;
Tiger t;
zoo.insert(zoo.end(), { c, t });
return 0;
}
```
Indeed, if we run the code above, the vector `zoo` now only has two indistinguishable `Feline`.
## Pointers & References Prevent Slicing
There are two ways we can prevent object slicing, using either references or pointers.
>👉 If there's one rule to remember about them, it's to use references where you can, and pointers where you must.
Coming back two our previous example, let's make a `Feline` `meow`.
```cpp
int main()
{
Cat c;
// USING A REFERENCE
Feline& f1 = c;
/*
🚫 "Compile-time error: static type is Feline&"
A different error ! This is progress.
f1's static type is Feline&; it has no members meow()
*/
f1.meow();
// USING A POINTER
Feline* f2 = &c; // address-of operator
/*
🚫 "Compile-time error: static type is Feline*"
f2's static type is Feline*; it too has no members meow()
*/
f2->meow(); // -> calls a member method through a pointer
return 0;
}
```
This still doesn't work ! However, now we see a different error, that demonstrates the Cat identity wasn't sliced away.
So now we know how to prevent object slicing.
## Polymorphic Data Containers
From this we can infer that preventing object slicing in a `vector` must be possible, too. It is.
There's, however, one distinction: You cannot create an `std::vector`, or any other standard container, of references.
Think of references as nicknames. A nickname is not the person it refers too, even if everybody understands who you're talking about.
This is the case because references themselves are not objects, but aliases for other objects that must be initialized to refer to an object when they're created. They cannot be empty or reassigned to refer to something else.
You can probably see this is a recursive problem: a vector of references would need to hold references to objects somewhere else.
Meanwhile, an `std::vector` needs to be able to move its elements around in memory, which require the ability to reassign them, something references can't do.
Since references don't make any sense for our use case, we're left with pointers then. That way, our objects will not be truncated, thus retaining their specificities.
```cpp
class Feline { /* ... */ };
class Tiger : public Feline { /* ... */ };
class Cat : public Feline { /* ... */ };
class Zoo
{
public:
void introduce(std::initializer_list<Feline*> felines)
{
zoo.insert(zoo.end(), felines);
}
private:
std::vector<Feline*> zoo;
};
int main()
{
Zoo z;
/*
✅ Successfully inserts both a Cat and a Tiger into the zoo !
They have retained their properties.
*/
z.introduce({ new Cat(), new Tiger() });
return 0;
}
```
We successfully created an **heterogenous data container**.
However, there's now a bug in our program.
Because we dynamically allocate memory using the word `new`, we are now responsible for deallocating it as well. Failing to do so creates memory leaks.
This must happen when `zoo` goes out of scope and gets destroyed.
```cpp
class Zoo
{
public:
~Zoo()
{
for (Feline* feline : zoo)
{
delete feline;
}
}
void introduce(std::initializer_list<Feline*> felines)
{
zoo.insert(zoo.end(), felines);
}
private:
std::vector<Feline*> zoo;
};
```
A better way would be not to have to worry about managing memory at all. This we can achieve by leveraging **smart pointers** made available in recent editions of C++ to address this recurring issue.
That's beyond the scope of this section, but you can head over to the [[Pointers & References|pointers & references]] one to learn about them.
Going back to the `zoo`, we can notice something else. What's true for variables and containers is also true of function parameters.
Indeed, the function `void introduce(std::initializer_list<Feline*>)` accepts a list of `Feline` objects, without slicing them either. This means we can write a single function for heterogenous data types. We're getting closer !
>⚠️ There's one thing to be wary of. If you forget to make function parameters references, you're passing objects by value. If you pass objects by value, you're calling the copy constructor. And because the parameter static type will be `Feline`, its constructor will be called, and we revert back to object slicing. We haven't defined what's a static type yet, but don't worry, that's the next part.
>
>Had our function's signature been `void introduce(std::initializer_list<Feline>)`, every `Cat` and `Tiger` would have been sliced down to a `Feline` before even entering the function. This is why passing polymorphic objects by pointer or reference is so critical.