Wat is polymorfisme
De mogelijkheid om in algemene termen over objecten van verschillende gerelateerde typen te praten.
Voorbeeld:De class MidiEvent is een algemeen type waar de verschillende MIDI events (note events, command events) van afgeleid worden. MidiEvent beschrijft geen specifieke MIDI event, maar doet alleen dienst als generiek type. MidiEvent bevat algemene functionaliteit die voor diverse echte MIDI events hetzelfde is, zoals aangeven van het tijdstip van binnenkomst of wanneer het naar de synthesizer gestuurd moet worden.
We kunnen nu een event list maken die events van verschillende typen MIDI events bevat. Omdat die verschillende typen wel allemaal MIDI events zijn spreken we over een event list van MIDI events, zonder aan te geven om wat voor events het precies gaat:
We kunnen nu allerlei routines maken die dingen met MIDI events doen zoals toevoegen, weghalen of sorteren op tijdstip van binnenkomst. Daarvoor hebben we alleen de algemene functionaliteit uit het MidiEvent-gedeelte van de events nodig en hoeven we nog niet te weten wat de specifieke inhoud van de events is.
Stel nu dat we de event list willen beluisteren. Dan moet van elke event wel bekend zijn wat het precies voor event is zodat de juiste afspeel-functie aangeroepen wordt. We hebben dan de functionaliteit uit het MidiNoteEvent-gedeelte of MidiCommandEvent-gedeelte van een event nodig. Dat is waar polymorfisme van pas komt: via een afspeelfunctie in het MidiEvent-gedeelte kunnen we bij een afspeelfunctie in het MidiNoteEvent gedeelte. Zo kunnen we de lijst met MIDI events afspelen, waarbij automatisch tijdens runtime voor elke event de juiste afspeelfunctie wordt uitgevoerd.
Hoe werkt dit in C++
In C++ kun je een derived class pointer laten wijzen naar een object van type derived class en beschikken over alle functionaliteit van het object.
Je kunt echter ook een base class pointer laten wijzen naar een object van type derived class. Het object zelf verandert hier niet door, dus de derived-class onderdelen blijven gewoon bestaan, alleen kun je er via de base-class pointer niet direct bij:
Polymorfisme biedt de mogelijkheid om met een object van een afgeleide class te werken alsof het van het type base class is en via algemene functies in het base class gedeelte de corresponderende functies in het derived class gedeelte uit te voeren.
Dynamic binding, late binding
Polymorfisme wordt ook wel dynamic binding of late binding genoemd. Ik leg zo uit waarom.
Wanneer je zonder pointer met objecten werkt, bijvoorbeeld
MyClass myobject;
myobject.setvalue(42);
myobject.show();
dan kan de compiler het type van het object al vaststellen. Tijdens het compileren wordt dan al besloten welke member-functies van het object worden aangeroepen: die uit het base-class deel of uit het derived-class deel. Dat wordt static binding genoemd: het verbinden van de implementatie (de body) van een functie met zijn aanroep gebeurt statisch, dus op compile-time. Bij de uitleg van inheritance hebben we al besproken wanneer welke functie-body gebruikt wordt.
Om late binding te gebruiken moet je met een pointer of reference naar een object wijzen. In dat geval is namelijk op compile-time alleen het type van de pointer bekend, maar niet naar wat voor object de pointer wijst, want dat kan steeds veranderen: een base-class pointer mag wijzen naar een object van type base-class maar ook naar een object van type derived-class. Er zal dus tijdens het uitvoeren van het programma bekeken moeten worden naar wat voor type object een pointer wijst wanneer je een member-functie van het object aanroept. Dat hoeven we niet zelf te doen maar is een mechanisme dat in C++ automatisch gaat.
N.B.: Het mechanisme van late binding treedt in werking wanneer je via een base-class pointer of reference naar een derived-class object wijst en een virtual functie uit het base-class deel aanroept die overridden is in de afgeleide class.
class myBase
{
public:
myBase();
virtual void show();
};
class myDerived : public myBase
{
public:
myDerived();
void show();
};
myBase* bp; // Maak een base class pointer
bp = new myDerived; // En laat die wijzen naar een derived class object
bp->show(); // welke show() wordt nu aangeroepen ?
Door het keyword virtual voor de functie show() in de base class wordt in
dit voorbeeld de functie show() uit de derived class aangeroepen. Tijdens
runtime wordt gekeken naar welk type object de base class pointer nou
eigenlijk wijst en, als het van het type derived class is, of daar ook een
functie show() bestaat en zo ja dan wordt die aangeroepen. Als de functie
show() in de derived class niet bestaat dan wordt show() uit de base class
aangeroepen.
Je hoeft virtual functies niet te overriden ! Als je dat
niet doet wordt altijd de functie uit de base class aangeroepen.
"pure virtual" functies moet je wel overriden, zie verderop. Dat is nogal
logisch want die kunnen zelf niet worden aangeroepen en als er dan geen
variant in de afgeleide class is dan is er niks om aan te roepen.
Omdat in dit geval pas tijdens runtime de aanroep van een functie wordt gekoppeld aan de juiste functie-body spreken we van late binding.
Terug naar MIDI
Even terug naar het voorbeeld met MidiEvent. Hier is MidiEvent de base class en MidiNoteEvent en MidiCommandEvent zijn derived classes. Allemaal hebben ze een functie play().
Maak je nu een array van het type MidiEvent-pointer, dan kun je elk element van deze array laten wijzen naar een object van het type MidiEvent, MidiNoteEvent of MidiCommandEvent. Via zo'n pointer kun je alleen bij de base-class onderdelen van die objecten, behalve in het geval van de functie play() want die is in de base class virtual gemaakt.
Roep je de functie play() aan via de base class pointer dan wordt tijdens runtime gekeken welke functie-body uitgevoerd moet worden: als het object een MidiNoteEvent is dan wordt de play() uit het MidiNoteEvent-deel uitgevoerd. Als het een MidiEvent is dan wordt play() uit MidiEvent uitgevoerd.
Pointer fun
Welk type pointer mag naar welk type object wijzen ?
myBase b; // Base class object b
myDerived d; // Derived class object d
myBase* bptr; // Base class pointer bptr
myDerived* dptr; // Derived class pointer dptr
bptr mag wijzen naar b en naar d.
dptr mag alleen wijzen naar d.
Een typecast van base class pointer naar derived class pointer kun je doen met dynamic_cast.
Virtual function signature
Als je dynamic binding wilt gebruiken moet de signature (de combinatie van functienaam, returnwaarde en parameterlijst) van de virtual functie in de base class identiek zijn aan die van de gelijknamige functie in de derived class.
"Pure virtual" functies en abstract classes
Als een virtual functie in de base class niks moet doen kun je hem pure virtual maken door er "= 0" achter te zetten:
virtual void show() = 0;
Zo'n functie heeft dus geen implementatie. Dit wordt gebruikt om af te dwingen dat een base class alleen wordt gebruikt om classes van af te leiden.
- Van een class met pure virtual functie(s) kun je geen objecten maken. Zo'n class wordt een "abstract class" genoemd.
- Pure virtual functies MOETEN in een derived class geimplementeerd worden, tenzij je wilt dat de derived class ook een abstract class is.
Virtual destructor
Een virtual destructor in de base class waarborgt het aanroepen van de destructor van afgeleide classes wanneer niet op compile time kan worden vastgesteld welke destructoren moeten worden aangeroepen.Multiple inheritance
Wanneer een class eigenschappen erft van meerdere base classes spreken we van multiple inheritance:
Multiple inheritance kan zeer subtiele problemen geven, bijvoorbeeld wanneer beide base classes elementen met dezelfde naam bevatten, of wanneer beide base classes afgeleid zijn van dezelfde base class daar weer boven, zodat objecten van de laagste afgeleide class meerdere copieen van een element uit de top level base class bevatten.
In talen als Java is multiple inheritance niet mogelijk om dergelijke diep verborgen fouten te voorkomen.
Virtual base class
Gegeven de volgende situatie:Een object van class Multiple kan twee kopieën van elementen van class Base bevatten. Dat leidt tot een verwarrende situatie want de compiler weet niet welke van de twee hij moet nemen.
De oplossing is om beide derived classes "virtual" te laten erven van Base. Op die manier delen ze dezelfde base class elementen.
class Derived_1 : virtual public Base
{
....