Mentions légales du service

Skip to content
Snippets Groups Projects
Commit 33d80635 authored by GILLES Sebastien's avatar GILLES Sebastien
Browse files

Rearrange better code and explanation for dynamic_cast. Thanks Vincent for the suggestion!

parent 802718cf
No related tags found
No related merge requests found
Pipeline #1071714 passed
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
# [Getting started in C++](./) - [Object programming](./0-main.ipynb) - [Polymorphism](./7-polymorphism.ipynb) # [Getting started in C++](./) - [Object programming](./0-main.ipynb) - [Polymorphism](./7-polymorphism.ipynb)
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
## Polymorphism ## Polymorphism
### Naïve approach to underline the need ### 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. [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 first idea would be to cram all items in a container whose type is the base class:
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
class NotPolymorphicVehicle class NotPolymorphicVehicle
{ {
public: public:
NotPolymorphicVehicle() = default; NotPolymorphicVehicle() = default;
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <string> #include <string>
class NotPolymorphicCar : public NotPolymorphicVehicle class NotPolymorphicCar : public NotPolymorphicVehicle
{ {
public: public:
NotPolymorphicCar(); NotPolymorphicCar();
void Print() const; void Print() const;
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
void NotPolymorphicCar::Print() const void NotPolymorphicCar::Print() const
{ {
std::cout << "I'm a car!" << std::endl; std::cout << "I'm a car!" << std::endl;
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <string> #include <string>
class NotPolymorphicBicycle : public NotPolymorphicVehicle class NotPolymorphicBicycle : public NotPolymorphicVehicle
{ {
public: public:
NotPolymorphicBicycle(); NotPolymorphicBicycle();
void Print() const; void Print() const;
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
void NotPolymorphicBicycle::Print() const void NotPolymorphicBicycle::Print() const
{ {
std::cout << "I'm a bike!" << std::endl; std::cout << "I'm a bike!" << std::endl;
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
{ {
NotPolymorphicBicycle b; NotPolymorphicBicycle b;
NotPolymorphicCar c; NotPolymorphicCar c;
NotPolymorphicVehicle list[2] = { b, c }; NotPolymorphicVehicle list[2] = { b, c };
list[0].Print(); // COMPILATION ERROR list[0].Print(); // COMPILATION ERROR
} }
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
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. 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: Defining `Print()` in the base class would not work either:
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
class AlsoNotPolymorphicVehicle class AlsoNotPolymorphicVehicle
{ {
public: public:
AlsoNotPolymorphicVehicle() = default; AlsoNotPolymorphicVehicle() = default;
void Print() const; void Print() const;
}; };
class AlsoNotPolymorphicCar : public AlsoNotPolymorphicVehicle class AlsoNotPolymorphicCar : public AlsoNotPolymorphicVehicle
{ {
public: public:
AlsoNotPolymorphicCar() = default; AlsoNotPolymorphicCar() = default;
void Print() const; void Print() const;
}; };
class AlsoNotPolymorphicBicycle : public AlsoNotPolymorphicVehicle class AlsoNotPolymorphicBicycle : public AlsoNotPolymorphicVehicle
{ {
public: public:
AlsoNotPolymorphicBicycle() = default; AlsoNotPolymorphicBicycle() = default;
void Print() const; void Print() const;
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
void AlsoNotPolymorphicVehicle::Print() const void AlsoNotPolymorphicVehicle::Print() const
{ {
std::cout << "I am... hopefully a polymorphic vehicle?" << std::endl; std::cout << "I am... hopefully a polymorphic vehicle?" << std::endl;
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
void AlsoNotPolymorphicCar::Print() const void AlsoNotPolymorphicCar::Print() const
{ {
std::cout << "I'm a car!" << std::endl; std::cout << "I'm a car!" << std::endl;
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
void AlsoNotPolymorphicBicycle::Print() const void AlsoNotPolymorphicBicycle::Print() const
{ {
std::cout << "I'm a bike!" << std::endl; std::cout << "I'm a bike!" << std::endl;
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
{ {
AlsoNotPolymorphicBicycle b; AlsoNotPolymorphicBicycle b;
AlsoNotPolymorphicCar c; AlsoNotPolymorphicCar c;
AlsoNotPolymorphicVehicle list[2] = { b, c }; AlsoNotPolymorphicVehicle list[2] = { b, c };
for (auto i = 0ul; i < 2ul; ++i) for (auto i = 0ul; i < 2ul; ++i)
list[i].Print(); // No compilation error, but clearly not what we intended... list[i].Print(); // No compilation error, but clearly not what we intended...
} }
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
So far, the perspectives aren't rosy: 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. * 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. * 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 ### `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. 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: Almost all methods might be virtual, with very few exceptions:
* Static methods * 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)) * 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) * 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...). That means even the destructor may be virtual (and probably should - we'll go back to that as well...).
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
class PolymorphicButClumsyVehicle class PolymorphicButClumsyVehicle
{ {
public: public:
PolymorphicButClumsyVehicle() = default; PolymorphicButClumsyVehicle() = default;
virtual void Print() const; virtual void Print() const;
}; };
class PolymorphicButClumsyCar : public PolymorphicButClumsyVehicle class PolymorphicButClumsyCar : public PolymorphicButClumsyVehicle
{ {
public: public:
PolymorphicButClumsyCar() = default; PolymorphicButClumsyCar() = default;
virtual void Print() const; virtual void Print() const;
}; };
class PolymorphicButClumsyBicycle : public PolymorphicButClumsyVehicle class PolymorphicButClumsyBicycle : public PolymorphicButClumsyVehicle
{ {
public: public:
PolymorphicButClumsyBicycle() = default; PolymorphicButClumsyBicycle() = default;
virtual void Print() const; virtual void Print() const;
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
void PolymorphicButClumsyVehicle::Print() const // Please notice: no `virtual` on definition! void PolymorphicButClumsyVehicle::Print() const // Please notice: no `virtual` on definition!
{ {
std::cout << "I am... hopefully a polymorphic vehicle?" << std::endl; std::cout << "I am... hopefully a polymorphic vehicle?" << std::endl;
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
void PolymorphicButClumsyCar::Print() const void PolymorphicButClumsyCar::Print() const
{ {
std::cout << "I am a car!" << std::endl; std::cout << "I am a car!" << std::endl;
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
void PolymorphicButClumsyBicycle::Print() const void PolymorphicButClumsyBicycle::Print() const
{ {
std::cout << "I am a bike!" << std::endl; std::cout << "I am a bike!" << std::endl;
} }
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
### `virtual` work only with pointers or references ### `virtual` work only with pointers or references
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
{ {
PolymorphicButClumsyBicycle b; PolymorphicButClumsyBicycle b;
PolymorphicButClumsyCar c; PolymorphicButClumsyCar c;
PolymorphicButClumsyVehicle list[2] = { b, c }; PolymorphicButClumsyVehicle list[2] = { b, c };
for (auto i = 0ul; i < 2ul; ++i) for (auto i = 0ul; i < 2ul; ++i)
list[i].Print(); // No compilation error, but clearly not what we intended... list[i].Print(); // No compilation error, but clearly not what we intended...
} }
``` ```
%% Cell type:markdown id: tags: %% 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. 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: To use the **dynamic binding**, we need to use either references or pointers. Let's do that:
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
{ {
PolymorphicButClumsyVehicle* b = new PolymorphicButClumsyBicycle; PolymorphicButClumsyVehicle* b = new PolymorphicButClumsyBicycle;
PolymorphicButClumsyVehicle* c = new PolymorphicButClumsyCar; PolymorphicButClumsyVehicle* c = new PolymorphicButClumsyCar;
PolymorphicButClumsyVehicle* list[2] = { b, c }; PolymorphicButClumsyVehicle* list[2] = { b, c };
for (auto i = 0ul; i < 2ul; ++i) for (auto i = 0ul; i < 2ul; ++i)
list[i]->Print(); list[i]->Print();
delete b; delete b;
delete c; delete c;
} }
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
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. 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.
We're nonetheless not completely done yet: We're nonetheless not completely done yet:
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
{ {
PolymorphicButClumsyVehicle* v = new PolymorphicButClumsyVehicle; PolymorphicButClumsyVehicle* v = new PolymorphicButClumsyVehicle;
PolymorphicButClumsyVehicle* b = new PolymorphicButClumsyBicycle; PolymorphicButClumsyVehicle* b = new PolymorphicButClumsyBicycle;
PolymorphicButClumsyVehicle* c = new PolymorphicButClumsyCar; PolymorphicButClumsyVehicle* c = new PolymorphicButClumsyCar;
PolymorphicButClumsyVehicle* list[3] = { v, b, c }; PolymorphicButClumsyVehicle* list[3] = { v, b, c };
for (auto i = 0ul; i < 3ul; ++i) for (auto i = 0ul; i < 3ul; ++i)
list[i]->Print(); list[i]->Print();
delete v; delete v;
delete b; delete b;
delete c; delete c;
} }
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
### Abstract class and pure virtual methods ### Abstract class and pure virtual methods
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. 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. 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: %% Cell type:code id: tags:
``` c++ ``` c++
class PolymorphicVehicle class PolymorphicVehicle
{ {
public: public:
PolymorphicVehicle() = default; PolymorphicVehicle() = default;
virtual void Print() const = 0; // the only change from PolymorphicButClumsyVehicle! virtual void Print() const = 0; // the only change from PolymorphicButClumsyVehicle!
}; };
class PolymorphicCar : public PolymorphicVehicle class PolymorphicCar : public PolymorphicVehicle
{ {
public: public:
PolymorphicCar() = default; PolymorphicCar() = default;
virtual void Print() const; virtual void Print() const;
}; };
class PolymorphicBicycle : public PolymorphicVehicle class PolymorphicBicycle : public PolymorphicVehicle
{ {
public: public:
PolymorphicBicycle() = default; PolymorphicBicycle() = default;
virtual void Print() const; virtual void Print() const;
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
void PolymorphicCar::Print() const void PolymorphicCar::Print() const
{ {
std::cout << "I am a car!" << std::endl; std::cout << "I am a car!" << std::endl;
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
void PolymorphicBicycle::Print() const void PolymorphicBicycle::Print() const
{ {
std::cout << "I am a bike!" << std::endl; std::cout << "I am a bike!" << std::endl;
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
{ {
PolymorphicVehicle v; // Compilation error: you can't instantiate an abstract class! PolymorphicVehicle v; // Compilation error: you can't instantiate an abstract class!
} }
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
But the following is fine: But the following is fine:
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
{ {
PolymorphicVehicle* b = new PolymorphicBicycle; PolymorphicVehicle* b = new PolymorphicBicycle;
PolymorphicVehicle* c = new PolymorphicCar; PolymorphicVehicle* c = new PolymorphicCar;
PolymorphicVehicle* list[2] = { b, c }; PolymorphicVehicle* list[2] = { b, c };
for (auto i = 0ul; i < 2ul; ++i) for (auto i = 0ul; i < 2ul; ++i)
list[i]->Print(); list[i]->Print();
delete b; delete b;
delete c; delete c;
} }
``` ```
%% Cell type:markdown id: tags: %% 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)_ _(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. **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: 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: %% Cell type:code id: tags:
``` c++ ``` c++
struct VirtualBase struct VirtualBase
{ {
virtual void Method() = 0; virtual void Method() = 0;
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
void VirtualBase::Method() void VirtualBase::Method()
{ {
std::cout << "Default implementation provided in abstract class." << std::endl; std::cout << "Default implementation provided in abstract class." << std::endl;
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
struct Concrete1 : public VirtualBase struct Concrete1 : public VirtualBase
{ {
virtual void Method(); virtual void Method();
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
struct Concrete2 : public VirtualBase struct Concrete2 : public VirtualBase
{ {
virtual void Method(); virtual void Method();
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
void Concrete1::Method() void Concrete1::Method()
{ {
VirtualBase::Method(); // call to the method defined in the base class 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 " 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; "in derived classes, such as here by these lines you are reading!" << std::endl;
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
void Concrete2::Method() void Concrete2::Method()
{ {
std::cout << "Overridden implementation." << std::endl; std::cout << "Overridden implementation." << std::endl;
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
{ {
std::cout << "====== Concrete 1: uses up the definition provided in base class =====" << std::endl; std::cout << "====== Concrete 1: uses up the definition provided in base class =====" << std::endl;
Concrete1 concrete1; Concrete1 concrete1;
concrete1.Method(); concrete1.Method();
std::cout << "\n====== Concrete 2: doesn't use the definition provided in base class =====" << std::endl; std::cout << "\n====== Concrete 2: doesn't use the definition provided in base class =====" << std::endl;
Concrete2 concrete2; Concrete2 concrete2;
concrete2.Method(); concrete2.Method();
} }
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
### override keyword ### 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. 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: 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: %% Cell type:code id: tags:
``` c++ ``` c++
class European class European
{ {
public: public:
European() = default; European() = default;
virtual void Print() const; virtual void Print() const;
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
void European::Print() const void European::Print() const
{ {
std::cout << "I'm European!" << std::endl; std::cout << "I'm European!" << std::endl;
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
class French : public European class French : public European
{ {
public: public:
French() = default; French() = default;
void Print() const; // virtual keyword is skipped here - and it's fine by the standard void Print() const; // virtual keyword is skipped here - and it's fine by the standard
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
void French::Print() const void French::Print() const
{ {
std::cout << "I'm French!" << std::endl; std::cout << "I'm French!" << std::endl;
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
{ {
European* european = new European; European* european = new European;
european->Print(); european->Print();
European* french = new French; European* french = new French;
french->Print(); french->Print();
} }
``` ```
%% Cell type:markdown id: tags: %% 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: 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: %% Cell type:code id: tags:
``` c++ ``` c++
class ThirstyFrench : public European class ThirstyFrench : public European
{ {
public: public:
ThirstyFrench() = default; ThirstyFrench() = default;
virtual void Pint() const; // typo here! And the optional `virtual` doesn't help avoid it... virtual void Pint() const; // typo here! And the optional `virtual` doesn't help avoid it...
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
void ThirstyFrench::Pint() const void ThirstyFrench::Pint() const
{ {
std::cout << "I'm French!" << std::endl; std::cout << "I'm French!" << std::endl;
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
{ {
ThirstyFrench* french = new ThirstyFrench; ThirstyFrench* french = new ThirstyFrench;
french->Print(); french->Print();
} }
``` ```
%% Cell type:markdown id: tags: %% 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: 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: %% Cell type:code id: tags:
``` c++ ``` c++
class ThirstyButCarefulFrench : public European class ThirstyButCarefulFrench : public European
{ {
public: public:
ThirstyButCarefulFrench() = default; ThirstyButCarefulFrench() = default;
void Pint() const override; // COMPILATION ERROR! void Pint() const override; // COMPILATION ERROR!
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
class ForgetfulFrench : public European class ForgetfulFrench : public European
{ {
public: public:
ForgetfulFrench() = default; ForgetfulFrench() = default;
virtual void Print() override; // COMPILATION ERROR! `const` is missing and therefore prototype doesn't match. virtual void Print() override; // COMPILATION ERROR! `const` is missing and therefore prototype doesn't match.
}; };
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
### Cost of virtuality ### 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. 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` ## `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? 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?
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <string> #include <string>
class PolymorphicVehicle class PolymorphicVehicle
{ {
public: public:
PolymorphicVehicle() = default; PolymorphicVehicle() = default;
virtual void Print() const = 0; virtual void Print() const = 0;
}; };
class PolymorphicThermicCar : public PolymorphicVehicle class PolymorphicThermicCar : public PolymorphicVehicle
{ {
public: public:
PolymorphicThermicCar(std::string&& oil_type); PolymorphicThermicCar(std::string&& oil_type);
virtual void Print() const; virtual void Print() const;
const std::string& GetOilType() const; const std::string& GetOilType() const;
private: private:
std::string oil_type_; std::string oil_type_;
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
PolymorphicThermicCar::PolymorphicThermicCar(std::string&& oil_type) PolymorphicThermicCar::PolymorphicThermicCar(std::string&& oil_type)
: PolymorphicVehicle(), : PolymorphicVehicle(),
oil_type_(oil_type) oil_type_(oil_type)
{ } { }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
void PolymorphicThermicCar::Print() const void PolymorphicThermicCar::Print() const
{ {
std::cout << "I'm a car that uses up " << GetOilType() << std::endl; std::cout << "I'm a car that uses up " << GetOilType() << std::endl;
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
const std::string& PolymorphicThermicCar::GetOilType() const const std::string& PolymorphicThermicCar::GetOilType() const
{ {
return oil_type_; return oil_type_;
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
{ {
PolymorphicVehicle* c = new PolymorphicThermicCar("gazole"); PolymorphicVehicle* c = new PolymorphicThermicCar("gazole");
std::cout << "Oil type = " << c->GetOilType() << std::endl; // COMPILATION ERROR: been there before... std::cout << "Oil type = " << c->GetOilType() << std::endl; // COMPILATION ERROR: been there before...
} }
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
You may in fact explicitly tells the `c` pointer needs to be interpreted as a `PolymorphicThermicCar`: You may in fact explicitly tells the `c` pointer needs to be interpreted as a `PolymorphicThermicCar`:
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
{ {
PolymorphicThermicCar* c = new PolymorphicThermicCar("gazole"); PolymorphicThermicCar* c = new PolymorphicThermicCar("gazole");
PolymorphicVehicle* list[1] = { c }; // Ok! PolymorphicVehicle* list[1] = { c }; // Ok!
auto c_corrected = dynamic_cast<PolymorphicThermicCar*>(list[0]); auto c_corrected = dynamic_cast<PolymorphicThermicCar*>(list[0]);
std::cout << "Oil type = " << c_corrected->GetOilType() << std::endl; std::cout << "Oil type = " << c_corrected->GetOilType() << std::endl;
} }
``` ```
%% Cell type:markdown id: tags: %% 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. 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. 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: %% Cell type:markdown id: tags:
### Error handling for `dynamic_cast` ### 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: 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:
- 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`.
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <memory> #include <memory>
struct Parent struct Parent
{ {
virtual ~Parent() = default; virtual ~Parent() = default;
}; };
struct Child1 : public Parent struct Child1 : public Parent
{ }; { };
struct Child2 : public Parent struct Child2 : public Parent
{ }; { };
``` ```
%% 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: %% Cell type:code id: tags:
``` c++ ``` c++
{ {
Parent* first = new Child1; Parent* first = new Child1;
Child2& cast = dynamic_cast<Child2&>(*first); // Should throw std::bad_cast exception! 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: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream>
{ {
Parent* first = new Child1; Parent* first = new Child1;
Child2* cast = dynamic_cast<Child2*>(first); // No throw, but cast should be `nullptr` Child2* cast = dynamic_cast<Child2*>(first); // No throw, but cast should be `nullptr`
assert(cast != nullptr); std::cout << "Is cast pointer nullptr? -> " << std::boolalpha << (cast == nullptr) << std::endl;
} }
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
## `final` keyword ## `final` keyword
If you need to specify a class that can't be derived, you may stick a `final` keyword in its declaration (from C++11 onward): If you need to specify a class that can't be derived, you may stick a `final` keyword in its declaration (from C++11 onward):
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
class UnderivableClass final class UnderivableClass final
{ }; { };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
class ITryAnyway : public UnderivableClass // COMPILATION ERROR! class ITryAnyway : public UnderivableClass // COMPILATION ERROR!
{ }; { };
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
The keyword may also be used only for one or several virtual methods: The keyword may also be used only for one or several virtual methods:
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
struct DerivableClass struct DerivableClass
{ {
virtual void Method1(); virtual void Method1();
virtual void Method2(); virtual void Method2();
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
struct DerivedClassFirstLevel : public DerivableClass struct DerivedClassFirstLevel : public DerivableClass
{ {
virtual void Method1() override final; virtual void Method1() override final;
virtual void Method2() override; virtual void Method2() override;
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
struct DerivedClassSecondLevel : public DerivedClassFirstLevel struct DerivedClassSecondLevel : public DerivedClassFirstLevel
{ {
virtual void Method2() override; // ok virtual void Method2() override; // ok
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
struct DerivedClassSecondLevelIgnoreFinal : public DerivedClassFirstLevel struct DerivedClassSecondLevelIgnoreFinal : public DerivedClassFirstLevel
{ {
virtual void Method1() override; // COMPILATION ERROR! virtual void Method1() override; // COMPILATION ERROR!
}; };
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
## Virtual destructor to avoid partial destruction ## 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: 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:
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
struct BaseClass3 struct BaseClass3
{ {
BaseClass3(); BaseClass3();
~BaseClass3(); ~BaseClass3();
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <string> #include <string>
#include <iostream> #include <iostream>
BaseClass3::BaseClass3() BaseClass3::BaseClass3()
{ {
std::cout << "BaseClass3 constructor" << std::endl; std::cout << "BaseClass3 constructor" << std::endl;
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
BaseClass3::~BaseClass3() BaseClass3::~BaseClass3()
{ {
std::cout << "BaseClass3 destructor" << std::endl; std::cout << "BaseClass3 destructor" << std::endl;
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
struct DerivedClass3 : public BaseClass3 struct DerivedClass3 : public BaseClass3
{ {
DerivedClass3(); DerivedClass3();
~DerivedClass3(); ~DerivedClass3();
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
DerivedClass3::DerivedClass3() DerivedClass3::DerivedClass3()
{ {
std::cout << "DerivedClass3 constructor" << std::endl; std::cout << "DerivedClass3 constructor" << std::endl;
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
DerivedClass3::~DerivedClass3() DerivedClass3::~DerivedClass3()
{ {
std::cout << "DerivedClass3 destructor" << std::endl; std::cout << "DerivedClass3 destructor" << std::endl;
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
{ {
std::cout << "Here all should be well: " << std::endl; std::cout << "Here all should be well: " << std::endl;
{ {
DerivedClass3 obj; DerivedClass3 obj;
} }
std::cout << std::endl << "But there not so much, see the missing destructor call! " << std::endl; std::cout << std::endl << "But there not so much, see the missing destructor call! " << std::endl;
{ {
BaseClass3* obj = new DerivedClass3; BaseClass3* obj = new DerivedClass3;
delete obj; delete obj;
} }
} }
``` ```
%% Cell type:markdown id: tags: %% 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. 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. 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? ### Good practice: should my destructor be virtual?
So to put in a nutshell, 99 % of the time: 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 a class of yours is intended to be inherited from, make its destructor `virtual`.
* If not, mark the class as `final`. * If not, mark the class as `final`.
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
## Good practice: never call a virtual method in a constructor ## 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++)... 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). 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: %% Cell type:code id: tags:
``` c++ ``` c++
#include <string> #include <string>
struct BaseClass4 struct BaseClass4
{ {
BaseClass4(); BaseClass4();
virtual std::string ClassName() const; virtual std::string ClassName() const;
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
struct DerivedClass4 : public BaseClass4 struct DerivedClass4 : public BaseClass4
{ {
DerivedClass4() = default; DerivedClass4() = default;
virtual std::string ClassName() const; virtual std::string ClassName() const;
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
BaseClass4::BaseClass4() BaseClass4::BaseClass4()
{ {
std::cout << "Hello! I'm " << ClassName() << std::endl; std::cout << "Hello! I'm " << ClassName() << std::endl;
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
std::string BaseClass4::ClassName() const std::string BaseClass4::ClassName() const
{ {
return "BaseClass4"; return "BaseClass4";
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
std::string DerivedClass4::ClassName() const std::string DerivedClass4::ClassName() const
{ {
return "DerivedClass4"; return "DerivedClass4";
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
{ {
DerivedClass4 object; // not working by stack allocation... DerivedClass4 object; // not working by stack allocation...
DerivedClass4* object2 = new DerivedClass4; // neither by heap allocation! DerivedClass4* object2 = new DerivedClass4; // neither by heap allocation!
} }
``` ```
%% Cell type:markdown id: tags: %% 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)). 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: 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: %% Cell type:code id: tags:
``` c++ ``` c++
#include <string> #include <string>
struct BaseClass5 struct BaseClass5
{ {
BaseClass5() = default; BaseClass5() = default;
void Init(); void Init();
virtual std::string ClassName() const; virtual std::string ClassName() const;
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
struct DerivedClass5 : public BaseClass5 struct DerivedClass5 : public BaseClass5
{ {
DerivedClass5() = default; DerivedClass5() = default;
virtual std::string ClassName() const; virtual std::string ClassName() const;
}; };
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
#include <iostream> #include <iostream>
void BaseClass5::Init() void BaseClass5::Init()
{ {
std::cout << "Hello! I'm " << ClassName() << std::endl; std::cout << "Hello! I'm " << ClassName() << std::endl;
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
std::string BaseClass5::ClassName() const std::string BaseClass5::ClassName() const
{ {
return "BaseClass5"; return "BaseClass5";
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
std::string DerivedClass5::ClassName() const std::string DerivedClass5::ClassName() const
{ {
return "DerivedClass5"; return "DerivedClass5";
} }
``` ```
%% Cell type:code id: tags: %% Cell type:code id: tags:
``` c++ ``` c++
{ {
DerivedClass5 object; // ok by stack allocation DerivedClass5 object; // ok by stack allocation
object.Init(); object.Init();
DerivedClass5* object2 = new DerivedClass5; // same by heap allocation DerivedClass5* object2 = new DerivedClass5; // same by heap allocation
object2->Init(); object2->Init();
} }
``` ```
%% Cell type:markdown id: tags: %% 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). 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).
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
[© Copyright](../COPYRIGHT.md) [© Copyright](../COPYRIGHT.md)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment