The makeIntroduction function that we used to demonstrate polymorphism took a const Person& parameter. As discussed earlier, it is always a good idea to take complex objects by reference to avoid making temporary copies of them. However, when trying to get polymorphic behavior, working with references or pointers is essential.
Student s("Alex", 20, "Computer Science");
Person p = s;
The second line says βMake a new Person object called p and make it be a copy of the Student object s.β The resulting object p will just be a Person. That means there will only be storage for the things we need for a plain Person. There is no room to store anything extra provided by Student. So that data from s will be βslicedβ off as the copy is made. This is known as object slicing.
Thus if we rewrote the makeIntroduction function to use pass by value, it would no longer be able to produce polymorphic behavior. The object we are working with is just a Person and will never have extra information.
void makeIntroduction(Person person) {
// Person is a COPY of the object passed in - any extra data was sliced off
person.introduce(); // Can only do Person::introduce()
// Consider using a reference to avoid slicing
}
The original makeIntroduction uses pass by const reference, so the person parameter is not a copy of what is passed in, it is a reference to the original. Nothing needed to be sliced. That means at run time we can check βWhat kind of object is this? Which version of introduce should we use?β
The same basic idea applies to pointers. If we had a pointer p to a Person object, and the object it points to happens to be a Student, using p->introduce() will dynamically find the version of introduce() that is most appropriate for the actual object type.
Student s("Alex", 20, "Computer Science");
Person& pref = s; // pref is a reference to a Person that happens to be a Student
Person* pPtr = &s; // pPtr is a pointer to a Person that happens to be a Student
Person p = s; // p is a copy of the Student object, but only the Person part
In contrast, we are not allowed to take a more general base class and treat it as a derived class. For plain objects, this is entirely logical. We canβt treat a plain Person as a Studentβa plain Person does not have a m_major and thus canβt do anything specific to a Student.
Person p("Wendy", 30);
Student& sRef = p; // Error! A Student & can't refer to a plain Person
Student* sPtr = &p; // Error! A Student* can't point to a plain Person
Student s = p; // Error! Can't copy a Person into a Student
However, if we have a pointer to a base class, it might point to a derived object. And we might need to access the extra information of that class. Imagine that Student has a method getMajor. And our code has been given p which is a Person*. We would like to check if that Person* actually points at a Student and, if so, call getMajor() on it. We canβt just write:
All the compiler knows for sure is that p has the memory address of some Person. It doesnβt know if it actually points to a Student. If it points to a plain Person object, there is no getMajor() function to make use of. So it refuses to compile that code.
But we can use a dynamic cast (remember that βdynamicβ means βat runtimeβ) to attempt to create a new pointer of type Student*. The syntax for doing so is:
We are asking the compiler to take the Person*p and examine it. If it does point to a Student object, we will store the memory address into the new variable sPtr. p and sPtr will store the same memory address and point at the same object. But because sPtr is a pointer to a Student the compiler can be confident that it points to a Student and it is safe to do things like call getMajor() on it.
Figure19.8.4.p and sPtr both point at the same Student. sPtr is known to be pointing at a Student. We donβt know what type of object p is pointing to.
If the check fails because p is not in fact pointing at a Student, the dynamic_cast will result in a nullptr. So, before we use the pointer produced by dynamic_cast, we should check to see if it is null or not:
Student* sPtr = dynamic_cast<Student*>(p);
if (sPtr) { // or if (sPtr != nullptr)
// dynamic_cast worked - sPtr is known to point to a Student
sPtr->getMajor();
} else {
// dynamic_cast failed - p was not a Student
cout << "p is not a Student" << endl;
}
Usually, polymorphism is a better way to make a derived class behave differently than its parent. When we used the introduce() function, we did not need to check if the object was a Student or not. We just called introduce() and the right version was used. But there are times when you absolutely need to know βIs this pointer pointing at a derived class?β. dynamic_cast<DerivedClass*>(pointer) is the way to check.