"You have to know if you're using `dynamic_cast` that error handling is not done the same depending on the access you use for your polymorphic object:\n",
"\n",
"- If you're casting through a reference, an exception is thrown if the cast is invalid (we'll cover exceptions in [this notebook](../5-UsefulConceptsAndSTL/1-ErrorHandling.ipynb)).\n",
"- If you're casting through a pointer, no exception thrown but the pointer will be set to `nullptr`.\n"
"You have to know, if you're using `dynamic_cast`, that error handling is not done the same depending on the access you use for your polymorphic object, as seen in this example:\n"
]
},
{
...
...
@@ -965,6 +962,13 @@
"{ };\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- If you're casting through a reference, an exception is thrown if the cast is invalid (we'll cover exceptions in [this notebook](../5-UsefulConceptsAndSTL/1-ErrorHandling.ipynb))."
]
},
{
"cell_type": "code",
"execution_count": null,
...
...
@@ -977,17 +981,26 @@
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- If you're casting through a pointer, no exception thrown but the pointer will be set to `nullptr`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#include <iostream>\n",
"\n",
"{\n",
" Parent* first = new Child1; \n",
" Child2* cast = dynamic_cast<Child2*>(first); // No throw, but cast should be `nullptr`\n",
# [Getting started in C++](./) - [Object programming](./0-main.ipynb) - [Polymorphism](./7-polymorphism.ipynb)
%% Cell type:markdown id: tags:
## Polymorphism
### Naïve approach to underline the need
[So far](./6-inheritance.ipynb), I have not yet shown how objects could be stored in the same container, which was a justification I gave when introducing inheritance.
The first idea would be to cram all items in a container whose type is the base class:
The issue here is that the objects are stored as `NotPolymorphicVehicle`, and this class doesn't know any method called `Print()`, even if all its children do.
Defining `Print()` in the base class would not work either:
std::cout<<"I am... hopefully a polymorphic vehicle?"<<std::endl;
}
```
%% Cell type:code id: tags:
``` c++
#include<iostream>
voidAlsoNotPolymorphicCar::Print()const
{
std::cout<<"I'm a car!"<<std::endl;
}
```
%% Cell type:code id: tags:
``` c++
#include<iostream>
voidAlsoNotPolymorphicBicycle::Print()const
{
std::cout<<"I'm a bike!"<<std::endl;
}
```
%% Cell type:code id: tags:
``` c++
#include<iostream>
{
AlsoNotPolymorphicBicycleb;
AlsoNotPolymorphicCarc;
AlsoNotPolymorphicVehiclelist[2]={b,c};
for(autoi=0ul;i<2ul;++i)
list[i].Print();// No compilation error, but clearly not what we intended...
}
```
%% Cell type:markdown id: tags:
So far, the perspectives aren't rosy:
* The base class needs to know all the methods beforehand: `Print()` had to be defined in the base class to make it compile.
* And even so, the result was clearly not what we hoped for: the dumb value provided in the base class was actually returned.
### `virtual ` keyword
The second point is easy to solve: there is a dedicated keyword named **virtual** in the language that may qualify a method and tell it is likely to be adapted or superseded in a derived class.
Almost all methods might be virtual, with very few exceptions:
* Static methods
* Constructors: no constructor can be virtual, and even more than that using a virtual method within a constructor won't work as expected (we'll come back to this [shortly](#Good-practice:-never-call-a-virtual-method-in-a-constructor))
* Template methods (we'll see that in part 4 of this lecture)
That means even the destructor may be virtual (and probably should - we'll go back to that as well...).
voidPolymorphicButClumsyVehicle::Print()const// Please notice: no `virtual` on definition!
{
std::cout<<"I am... hopefully a polymorphic vehicle?"<<std::endl;
}
```
%% Cell type:code id: tags:
``` c++
#include<iostream>
voidPolymorphicButClumsyCar::Print()const
{
std::cout<<"I am a car!"<<std::endl;
}
```
%% Cell type:code id: tags:
``` c++
#include<iostream>
voidPolymorphicButClumsyBicycle::Print()const
{
std::cout<<"I am a bike!"<<std::endl;
}
```
%% Cell type:markdown id: tags:
### `virtual` work only with pointers or references
%% Cell type:code id: tags:
``` c++
#include<iostream>
{
PolymorphicButClumsyBicycleb;
PolymorphicButClumsyCarc;
PolymorphicButClumsyVehiclelist[2]={b,c};
for(autoi=0ul;i<2ul;++i)
list[i].Print();// No compilation error, but clearly not what we intended...
}
```
%% Cell type:markdown id: tags:
Still not what was intended... That's because when you use the objects directly as we do here, **static binding** is used: the definitions seen in the base class are used directly because the resolution occurs at compilation time.
To use the **dynamic binding**, we need to use either references or pointers. Let's do that:
The fact that a `PolymorphicButClumsyVehicle` pointer is able to properly call the derived classes version is what is called **polymorphism**; it's at runtime that the decision to call `PolymorphicButClumsyVehicle::Print()` or `PolymorphicButClumsyCar::Print()` is taken.
Our issue here is that `PolymorphicButClumsyVehicle` has no business being instantiated directly: it is merely an **abstract class** which should never be instantiated but is there to provide a skeleton to more substantiated derived classes.
The mechanism to indicate that is to provide at least one **pure virtual method**: a method which prototype is given in the base class but that **must** be overridden in derived classes (at least if you want them to become concrete). The syntax to do so is to add `= 0` after the prototype.
%% Cell type:code id: tags:
``` c++
classPolymorphicVehicle
{
public:
PolymorphicVehicle()=default;
virtualvoidPrint()const=0;// the only change from PolymorphicButClumsyVehicle!
};
classPolymorphicCar:publicPolymorphicVehicle
{
public:
PolymorphicCar()=default;
virtualvoidPrint()const;
};
classPolymorphicBicycle:publicPolymorphicVehicle
{
public:
PolymorphicBicycle()=default;
virtualvoidPrint()const;
};
```
%% Cell type:code id: tags:
``` c++
#include<iostream>
voidPolymorphicCar::Print()const
{
std::cout<<"I am a car!"<<std::endl;
}
```
%% Cell type:code id: tags:
``` c++
#include<iostream>
voidPolymorphicBicycle::Print()const
{
std::cout<<"I am a bike!"<<std::endl;
}
```
%% Cell type:code id: tags:
``` c++
{
PolymorphicVehiclev;// Compilation error: you can't instantiate an abstract class!
}
```
%% Cell type:markdown id: tags:
But the following is fine:
%% Cell type:code id: tags:
``` c++
#include<iostream>
{
PolymorphicVehicle*b=newPolymorphicBicycle;
PolymorphicVehicle*c=newPolymorphicCar;
PolymorphicVehicle*list[2]={b,c};
for(autoi=0ul;i<2ul;++i)
list[i]->Print();
deleteb;
deletec;
}
```
%% Cell type:markdown id: tags:
_(don't worry if you get a warning - if so the compiler does its job well and we'll see shortly why)_
**Beware:** You **must** provide a definition for all non pure-virtual methods in your class. Not doing so leads to a somewhat cryptic error at link-time.
You are not required to provide a definition for a pure virtual method, and you won't most of the time... But you might provide one if you want to do so, for instance to provide an optional default instantiation for the method in derived classes:
%% Cell type:code id: tags:
``` c++
structVirtualBase
{
virtualvoidMethod()=0;
};
```
%% Cell type:code id: tags:
``` c++
#include<iostream>
voidVirtualBase::Method()
{
std::cout<<"Default implementation provided in abstract class."<<std::endl;
}
```
%% Cell type:code id: tags:
``` c++
structConcrete1:publicVirtualBase
{
virtualvoidMethod();
};
```
%% Cell type:code id: tags:
``` c++
structConcrete2:publicVirtualBase
{
virtualvoidMethod();
};
```
%% Cell type:code id: tags:
``` c++
voidConcrete1::Method()
{
VirtualBase::Method();// call to the method defined in the base class
std::cout<<"This enables providing a base behaviour that might be completed if needed "
"in derived classes, such as here by these lines you are reading!"<<std::endl;
std::cout<<"====== Concrete 1: uses up the definition provided in base class ====="<<std::endl;
Concrete1concrete1;
concrete1.Method();
std::cout<<"\n====== Concrete 2: doesn't use the definition provided in base class ====="<<std::endl;
Concrete2concrete2;
concrete2.Method();
}
```
%% Cell type:markdown id: tags:
### override keyword
We saw in previous section that to make a method virtual we need to add a `virtual` qualifier in front of its prototype.
I put it both in the base class and in the derived one, but in fact it is entirely up to the developer concerning the derived classes:
%% Cell type:code id: tags:
``` c++
classEuropean
{
public:
European()=default;
virtualvoidPrint()const;
};
```
%% Cell type:code id: tags:
``` c++
#include<iostream>
voidEuropean::Print()const
{
std::cout<<"I'm European!"<<std::endl;
};
```
%% Cell type:code id: tags:
``` c++
classFrench:publicEuropean
{
public:
French()=default;
voidPrint()const;// virtual keyword is skipped here - and it's fine by the standard
};
```
%% Cell type:code id: tags:
``` c++
#include<iostream>
voidFrench::Print()const
{
std::cout<<"I'm French!"<<std::endl;
};
```
%% Cell type:code id: tags:
``` c++
{
European*european=newEuropean;
european->Print();
European*french=newFrench;
french->Print();
}
```
%% Cell type:markdown id: tags:
But the drawback doing so is that we may forget the method is virtual... or we might do a typo when writing it! And in this case, the result is not what is expected:
%% Cell type:code id: tags:
``` c++
classThirstyFrench:publicEuropean
{
public:
ThirstyFrench()=default;
virtualvoidPint()const;// typo here! And the optional `virtual` doesn't help avoid it...
};
```
%% Cell type:code id: tags:
``` c++
voidThirstyFrench::Pint()const
{
std::cout<<"I'm French!"<<std::endl;
}
```
%% Cell type:code id: tags:
``` c++
{
ThirstyFrench*french=newThirstyFrench;
french->Print();
}
```
%% Cell type:markdown id: tags:
What would be nice is for the compiler to provide a way to secure against such errors... and that exactly what C++ 11 introduced with the `override` keyword. This keyword explicitly says we are declaring an override of a virtual method, and the code won't compile if the prototype doesn't match:
%% Cell type:code id: tags:
``` c++
classThirstyButCarefulFrench:publicEuropean
{
public:
ThirstyButCarefulFrench()=default;
voidPint()constoverride;// COMPILATION ERROR!
};
```
%% Cell type:code id: tags:
``` c++
classForgetfulFrench:publicEuropean
{
public:
ForgetfulFrench()=default;
virtualvoidPrint()override;// COMPILATION ERROR! `const` is missing and therefore prototype doesn't match.
};
```
%% Cell type:markdown id: tags:
### Cost of virtuality
You have to keep in mind there is a small cost related to the virtual behaviour: at each method call the program has to figure out which dynamic type to use. To be honest the true effective cost is quite blurry for me: some says it's not that important (see for instance [isocpp FAQ](https://isocpp.org/wiki/faq/virtual-functions)) while others will say you can't be serious if you're using them. I tend personally to avoid them in the part of my code where I want to crunch numbers fast and I use them preferably in the initialization phase.
## `dynamic_cast`
There is yet a question to ask: what if we want to use a method only defined in the derived class? For instance, if we add an attribute `oil_type_` that is not meaningful for all types of vehicles, can we access it?
std::cout<<"Oil type = "<<c_corrected->GetOilType()<<std::endl;
}
```
%% Cell type:markdown id: tags:
So you could devise a way to identify which is the dynamic type of your `PolymorphicVehicle` pointer and cast it dynamically to the rightful type so that extended API offered by the derived class is accessible.
If you find this clunky, you are not alone: by experience if you really need to resort to **dynamic_cast** it's probably that your data architecture needs some revision. But maybe the mileage vary for other developers, and it's useful to know the possibility exists.
%% Cell type:markdown id: tags:
### Error handling for `dynamic_cast`
You have to know if you're using `dynamic_cast` that error handling is not done the same depending on the access you use for your polymorphic object:
- If you're casting through a reference, an exception is thrown if the cast is invalid (we'll cover exceptions in [this notebook](../5-UsefulConceptsAndSTL/1-ErrorHandling.ipynb)).
- If you're casting through a pointer, no exception thrown but the pointer will be set to `nullptr`.
You have to know, if you're using `dynamic_cast`, that error handling is not done the same depending on the access you use for your polymorphic object, as seen in this example:
%% Cell type:code id: tags:
``` c++
#include<memory>
structParent
{
virtual~Parent()=default;
};
structChild1:publicParent
{};
structChild2:publicParent
{};
```
%% Cell type:markdown id: tags:
- If you're casting through a reference, an exception is thrown if the cast is invalid (we'll cover exceptions in [this notebook](../5-UsefulConceptsAndSTL/1-ErrorHandling.ipynb)).
%% Cell type:code id: tags:
``` c++
{
Parent*first=newChild1;
Child2&cast=dynamic_cast<Child2&>(*first);// Should throw std::bad_cast exception!
}
```
%% Cell type:markdown id: tags:
- If you're casting through a pointer, no exception thrown but the pointer will be set to `nullptr`.
%% Cell type:code id: tags:
``` c++
#include<iostream>
{
Parent*first=newChild1;
Child2*cast=dynamic_cast<Child2*>(first);// No throw, but cast should be `nullptr`
## Virtual destructor to avoid partial destruction
I indicated earlier that destructors might be virtual, but in fact I should have said that for most of the non final classes the destructor should be virtual:
std::cout<<"Here all should be well: "<<std::endl;
{
DerivedClass3obj;
}
std::cout<<std::endl<<"But there not so much, see the missing destructor call! "<<std::endl;
{
BaseClass3*obj=newDerivedClass3;
deleteobj;
}
}
```
%% Cell type:markdown id: tags:
You can see here the derived class destructor is not called! This means if you're for instance deallocating memory in this destructor, the memory will remain unduly allocated.
To circumvent this, you need to declare the destructor virtual in the base class. This way, the derived destructor will be properly called.
### Good practice: should my destructor be virtual?
So to put in a nutshell, 99 % of the time:
* If a class of yours is intended to be inherited from, make its destructor `virtual`.
* If not, mark the class as `final`.
%% Cell type:markdown id: tags:
## Good practice: never call a virtual method in a constructor
A very important point: I lost time years ago with this because I didn't read carefully enough item 9 of [Effective C++](../bibliography.ipynb#Effective-C++-/-More-Effective-C++)...
Due to the way construction occurs, never call a virtual method in a constructor: it won't perform the dynamic binding as you would like it to (and your compiler won't help you here).
%% Cell type:code id: tags:
``` c++
#include<string>
structBaseClass4
{
BaseClass4();
virtualstd::stringClassName()const;
};
```
%% Cell type:code id: tags:
``` c++
structDerivedClass4:publicBaseClass4
{
DerivedClass4()=default;
virtualstd::stringClassName()const;
};
```
%% Cell type:code id: tags:
``` c++
#include<iostream>
BaseClass4::BaseClass4()
{
std::cout<<"Hello! I'm "<<ClassName()<<std::endl;
}
```
%% Cell type:code id: tags:
``` c++
std::stringBaseClass4::ClassName()const
{
return"BaseClass4";
}
```
%% Cell type:code id: tags:
``` c++
std::stringDerivedClass4::ClassName()const
{
return"DerivedClass4";
}
```
%% Cell type:code id: tags:
``` c++
{
DerivedClass4object;// not working by stack allocation...
DerivedClass4*object2=newDerivedClass4;// neither by heap allocation!
}
```
%% Cell type:markdown id: tags:
There is unfortunately no way to circumvent this; just some hack tactics (see for instance the _VirtualConstructor_ idiom on the [isocpp FAQ](https://isocpp.org/wiki/faq/virtual-functions)).
When I need this functionality, I usually define an `Init()` method to call after the constructor, which includes the calls to virtual methods:
%% Cell type:code id: tags:
``` c++
#include<string>
structBaseClass5
{
BaseClass5()=default;
voidInit();
virtualstd::stringClassName()const;
};
```
%% Cell type:code id: tags:
``` c++
structDerivedClass5:publicBaseClass5
{
DerivedClass5()=default;
virtualstd::stringClassName()const;
};
```
%% Cell type:code id: tags:
``` c++
#include<iostream>
voidBaseClass5::Init()
{
std::cout<<"Hello! I'm "<<ClassName()<<std::endl;
}
```
%% Cell type:code id: tags:
``` c++
std::stringBaseClass5::ClassName()const
{
return"BaseClass5";
}
```
%% Cell type:code id: tags:
``` c++
std::stringDerivedClass5::ClassName()const
{
return"DerivedClass5";
}
```
%% Cell type:code id: tags:
``` c++
{
DerivedClass5object;// ok by stack allocation
object.Init();
DerivedClass5*object2=newDerivedClass5;// same by heap allocation
object2->Init();
}
```
%% Cell type:markdown id: tags:
But it's not perfect either: if the end-user forget to call the `Init()` method the class could be ill-constructed (to avoid this either I provide manually a check mechanism or I make sure the class is private stuff not intended to be used directly by the end-user).