Im Kontext der objektorientierten Software-Entwicklung bezieht sich Polymorphie auf Klassenfunktionen. Mehrere Klassen enthalten eine oder mehrere semantisch identische, aber klassenspezifisch unterschiedlich implementierte Funktionen. Der Aufruf der unterschiedlich implementierten Funktionen erfolgt dennoch gleichartig. Somit ergeben sich bei einem gleichartigen Aufruf zwei oder mehr Varianten der Funktionsausführung, auch Polymorphie genannt (Vielgestaltigkeit).
Bei der Anwendung der dynamischen Polymorphie ist es eine Laufzeit-Entscheidung, welche der möglichen Funktionen aufgerufen wird (dynamische / späte Bindung), und die aufrufbare Funktion ist gegen eine andere ebenfalls zur Laufzeit austauschbar. Bei der Anwendung der statischen Polymorphie ist es eine Compilezeit-Entscheidung, welche der möglichen Funktionen aufgerufen wird (statisch / frühe Bindung), und die aufrufbare Funktion ist gegen eine andere nur zur Compilezeit austauschbar.
Beispiel: Ressourcen-Zugriffsschutz
Die Klasse cCounter ist nicht thread-safe, da sie innerhalb verschiedener Funktionen auf die statische Member-Variable mStartValue zugreift. In der von cCounter spezialisierten (vererbten) Klasse cCounter_ThreadSafe soll durch verschiedenartige Betriebssystem-Mechanismen (Critical Section, Mutex, später optional Semaphore) der Ressourcenschutz für mStartValue flexibel austauschbar gewährleistet werden.
Für die gleichartigen Betriebssystem-Mechanismen-Klassen bietet sich die Anwendung der Polymorphie an: Die Klassen cCriticalSection, cMutex und cSemaphore enthalten semantisch identische Funktionen, zum Anfordern / Sperren lock() und zum Freigeben der Ressource unlock(). Die weiteren Detailunterscheidungen dieser Ressource-Schutzmechanismen stehen nicht im Mittelpunkt dieses Beitrags und werden hier nicht weiter erläutert.
Der folgende Abschnitt stellt drei konkrete Design- und Implementierungsansätze vor:
Interface und Assoziation
Template Parameter
CRTP (Curiously Recurring Template Pattern)
Neben der Unterscheidung zwischen dynamischer und statischer Polymorphie erfolgt ebenfalls ein Effizienzvergleich.
Interface und Assoziation
Die gemeinsame Interface-Klasse icResourceLocker abstrahiert die konkreten Ressource-Locker-Funktionen lock() und unlock(). In C++ sind dies rein virtuelle Funktionen.
class icResourceLocker
{
public:
virtual ~icResourceLocker() =default;
virtual void lock(void) =0;
virtual void unlock(void) =0;
};
Die beiden Ressource-Locker-Klassen cCriticalSection und cMutex implementieren das Interface durch eine Vererbung und überschreiben die Interface-Funktionen.
class cMutex : public icResourceLocker
{
public:
cMutex(void) =default;
~cMutex() override =default;
void lock(void) override;
void unlock(void) override;
private:
ecLockStatus_t mStatus = ecLockStatus_t::Unlocked;
};
Auf die Anbindung an ein konkretes Betriebssystem wurde hier verzichtet, da es nicht weiter zum Verständnis der Polymorphie erforderlich ist. Zur Symbolisierung des Ressourcen-Zustands ist die Klasse ecLockStatus_t als enum class enthalten.
void cMutex::lock(void)
{
// ... operating system call
mStatus = ecLockStatus_t::Locked;
showValue("\nMutex status = locked");
}
void cMutex::unlock(void)
{
// ... operating system call
mStatus = ecLockStatus_t::Unlocked;
showValue("\nMutex status = unlocked");
}
Die showValue()-Ausgabefunktion ist Teil der in den Beispielen enthaltenen kleinen Plattform zur Portierung auf beliebige Targets. Die Implementierungen von cCriticalSection und cMutex sind wesentlich identisch.
Die von cCounter abgeleitete Klasse cCounter__ThreadSafe definiert die Zugriffsfunktionen auf das statische Attribut mStartValue der Basisklasse mit Ressourcen-Schutz neu.
class cCounter_ThreadSafe : public cCounter
{
public:
cCounter_ThreadSafe(
icResourceLocker* const ptrResourceLocker = nullptr,
uint32_t const CountValue = 0);
~cCounter_ThreadSafe() override =default;
void setStartValue(uint32_t const StartValue);
uint32_t getStartValue(void);
void setCountToStartValue(void);
void setResourceLocker(icResourceLocker* const ptrResourceLocker);
private:
icResourceLocker* mptrResourceLocker;
};
Der Zugriff auf den Resource-Locker erfolgt über den Zeiger mptrResourceLocker vom Typ der Interface-Klasse icResourceLocker. Somit kann der Zeiger auf ein Objekt vom Typ cCrtiticalSection oder cMutex zeigen (siehe Liskov Substitution Principle [LSP]). Über den Konstruktor und / oder die cCounter_ThreadSafe::setResourceLocker()-Funktion erfolgt die Auswahl des gewünschten Resource-Lockers.
void cCounter_ThreadSafe::setCountToStartValue(void)
{
if (mptrResourceLocker != nullptr)
{
mptrResourceLocker->lock();
}
cCounter::setCountToStartValue();
if (mptrResourceLocker != nullptr)
{
mptrResourceLocker->unlock();
}
}
Das Beispiel der cCounter_ThreadSafe::setCountToStartValue()-Funktion zeigt exemplarisch die Anwendung des Resource-Schutzmechanismus: sperren, zugreifen und freigeben. Der Aufruf der Funktionen lock() und unlock() erfolgt jeweils über den Zeiger. Somit ist der konkrete Aufruf / Bindung vom Typ des Objektes abhängig, auf welches der Zeiger initialisiert ist. Dabei greift der Mechanismus der dynamischen Bindung mittels VMT (Virtual Method Tables). Hier zeigt sich die Anwendung der Polymorphie.
Die Anwendung instanziiert Resource-Locker-Objekte und initialisiert damit Thread-Safe Counter-Objekte.
Resource_Locker::cCriticalSection locCriticalSection{ };
Resource_Locker::cMutex locMutex{ };
Counter::cCounter_ThreadSafe locCounter_A{ &locCriticalSection };
Counter::cCounter_ThreadSafe locCounter_B{ &locMutex };
Unter anderem sind Funktionen, die den Ressourcen-Schutz enthalten, auf die Zählerobjekte anwendbar.
locCounter_A.setCountToStartValue();
locCounter_B.setCountToStartValue();
locCounter_A.count();
locCounter_B.count();
Mittels der Funktion cCounter_ThreadSafe::setResourceLocker() lassen sich die Resource-Locker-Objekte zur Laufzeit tauschen.
locCounter_A.setResourceLocker(&locMutex);
locCounter_B.setResourceLocker(&locCriticalSection);
Somit entspricht diese Variante einem Beispiel für die dynamische Polymorphie.
Template-Parameter
Im Vergleich zu der vorherigen Variante enthält der Resource-Locker kein Interface und keine virtuellen Funktionen mehr.
class cMutex
{
public:
cMutex(void) =default;
~cMutex() =default;
void lock(void);
void unlock(void);
private:
ecLockStatus_t mStatus = ecLockStatus_t::Unlocked;
};
Die weitere Implementierung von cMutex und cCriticalSection bleibt unverändert. Diese Variante ersetzt den vorherigen Interface-Zeiger in der Klasse cCounter_ThreadSafe der Template-Parameter ResourceLocker_T. Die spätere Typisierung dieses Parameters selektiert den zur Anwendung kommenden Resource-Locker, was wiederum einer Polymorphie entspricht.
template<typename ResourceLocker_T>
class tcCounter_ThreadSafe : public cCounter
{
public:
tcCounter_ThreadSafe(uint32_t const CountValue = 0);
~tcCounter_ThreadSafe() =default;
void setStartValue(uint32_t const StartValue);
uint32_t getStartValue(void);
void setCountToStartValue(void);
private:
ResourceLocker_T mResourceLocker;
};
Anstatt des Interface-Zeigers kommt ein konkretes Objekt vom Typ des Template-Parameters zur Anwendung; somit entfallen die sonst benötigten Zeigerabfragen.
template<typename ResourceLocker_T>
void tcCounter_ThreadSafe<ResourceLocker_T>::setCountToStartValue(void)
{
mResourceLocker.lock();
cCounter::setCountToStartValue();
mResourceLocker.unlock();
}
Die Bindung zwischen Objekt und Funktion erfolgt in diesem Fall statisch / früh, also zur Compilezeit. Die Anwendung instanziiert Objekte der Template-Klasse cCounter_ThreadSafe und initialisiert damit den Resource-Locker über den Template-Parameter.
tcCounter_ThreadSafe<cCriticalSection> locCounter_CriticalSection{ };
tcCounter_ThreadSafe<cMutex>
locCounter_Mutex{ };
Im Vergleich zur vorherigen Variante lassen sich hier die Resource-Locker nur zur Codier-/Compilezeit festlegen, aber nicht mehr zur Laufzeit tauschen. Somit entspricht diese Variante einem Beispiel für die statische Polymorphie.
Curiously Recurring Template Pattern
Ein Kritikpunkt an der Template-Parameter-Variante könnte der sein, dass eine gemeinsame Interface-Vereinbarung für die Resource-Locker-Klassen fehlt. Diesen Kritikpunkt eliminiert die Anwendung des Curiously Recurring Template Pattern (CRTP).
template<typename ResourceLocker_T>
class ticResourceLocker
{
public:
ticResourceLocker(void) =default;
~ticResourceLocker() =default;
void lock(void);
void unlock(void);
};
Auch diese Implementierungsvariante bleibt frei von virtuellen Funktionen. Die Interface-Klasse ist nun ebenfalls eine Template-Klasse, die als Template-Parameter den konkreten Resource-Locker erwartet.
} template<typename ResourceLocker_T>
void ticResourceLocker<ResourceLocker_T>::lock(void)
{
static_cast<ResourceLocker_T*>(this)->lock();
}
template<typename ResourceLocker_T>
void ticResourceLocker<ResourceLocker_T>::unlock(void)
{
static_cast<ResourceLocker_T*>(this)->unlock();
}
Die Funktionen ticResourceLocker::lock() und ticResourceLocker::unlock() rufen jeweils über den Template-Parameter die entsprechend in den abgeleiteten Klassen spezialisierten Funktionen auf.
class cMutex : public ticResourceLocker<cMutex>
{
public:
cMutex(void) =default;
~cMutex() =default;
void lock(void);
void unlock(void);
private:
ecLockStatus_t mStatus = ecLockStatus_t::Unlocked;
};
Bei der Vererbungsimplementierung wird bereits der Template-Parameter der Basisklasse mit dem Typ der erbenden Klasse spezifiziert (MixedIn). Dies ist am Beispiel der Klasse cMutex gezeigt.
void cMutex::lock(void)
{
// ... operating system call
mStatus = ecLockStatus_t::Locked;
showValue("\nMutex status = locked");
}
void cMutex::unlock(void)
{
// ... operating system call
mStatus = ecLockStatus_t::Unlocked;
showValue("\nMutex status = unlocked");
}
Die Funktionsimplementierungen von lock() und unlock() bleiben unverändert. Ebenfalls unverändert im Vergleich mit der Variante Template-Parameter bleibt die cCounter_ThreadSafe-Klassenimplementierung.
tcCounter_ThreadSafe<ticResourceLocker<cCriticalSection>>
locCounter_CriticalSection{ };
tcCounter_ThreadSafe<ticResourceLocker<cMutex>>
locCounter_Mutex{ };
Der Resource-Locker wird nur zur Codier-/Compilezeit festgelegt und ist nicht zur Laufzeit tauschbar. Somit entspricht diese Variante einem zweiten, erweiterten Beispiel für die statische Polymorphie.
Resümee
Die Implementierung der statischen Polymorphie ist aus der Perspektive der Software-Qualitätsmerkmale funktionale Sicherheit, Angriffssicherheit, Zuverlässigkeit und Verbrauchsverhalten immer die bessere Wahl. Der Preis, der dafür zu zahlen ist, ist der Verlust der positiven Unterstützung von Software-Qualitätsmerkmalen wie Änderbarkeit, Erweiterbarkeit oder Anpassbarkeit durch die dynamische Polymorphie.