C++11: pure virtual functions with override and final
In C++ können Member-Funktionen mit dem Schlüsselwort virtual
als virtuell (polymorph) gekennzeichnet werden. Virtuelle Funktionen können wiederum mit der Syntax =0
als pure
markiert werden. Mit C++11 kommen noch zwei weitere Auszeichnungen für virtuelle Funktionen hinzu: override
und final
. Berücksichtigt man dann noch die Zugriffsrechte public
und private
sowie die Ausprägungen in der Basis- und abgeleiteten Klasse, so ergeben sich zahlreiche Kombinationsmöglichkeiten. Aber sind die auch alle sinnvoll?
virtual or not virtual
C++ unterscheidet im Gegensatz zu vielen anderen modernen Programmiersprachen zwischen virtuellen und nicht virtuellen Funktionen. Damit soll dem Entwickler stets die Möglichkeit gegeben werden, zwischen Performance und Polymorphie abzuwägen. Eine nicht-statische Member-Funktion ist in C++ virtuell, wenn sie mit dem Schlüsselwort virtual
gekennzeichnet ist, oder wenn sie eine virtuelle Funktion einer Basisklasse überschreibt. Bei den speziellen Member-Funktionen ist die Situation noch ein wenig anders. So ist beispielsweise ein Destruktor virtuell, wenn er virtual
deklariert wurde, oder wenn der Destruktor irgendeiner Basisklasse virtuell ist. Ich gehe darauf allerdings nicht weiter ein und beschränke mich auf die normalen Funktionen.
struct base
{
virtual ~base() = default;
virtual void foo();
void bar();
};
struct derived : base
{
void foo(); // virtual and overrides base::foo
void bar(); // non-virtual and does not override base::bar
};
In dem Beispiel ist die Funktion foo
sowohl in der Basis- als auch in der abgeleiteten Klasse virtuell. Die Funktion bar
ist hingegen in keiner der beiden Klassen virtuell. Solcher Code verwirrt nicht nur Neueinsteiger sondern auch erfahrene C++‑Entwickler. Daher schreiben viele Programmierrichtlinien für C++ vor, dass virtuelle Funktionen explizit als solche gekennzeichnet werden müssen. Im folgenden Text beschränke ich mich nur noch auf virtuelle Funktionen – unabhängig davon ob sie auch explizit so deklariert wurden.
pure or not pure
Die pure virtual functions
werden syntaktisch durch =0
gekennzeichnet und entsprechen weitgehend den abstrakten Funktionen anderer Programmiersprachen. Nicht selbstverständlich ist, dass eine Funktion in einer abgeleiteten Klasse abstrakt sein kann und gleichzeitig eine nicht abstrakte Funktion einer Basis-Klasse überschreibt.
struct base
{
virtual ~base() = default;
virtual auto get_answer() -> int { return 42; }
};
struct derived : base
{
// pure virtual function overrides function in base class
virtual auto get_answer() -> int = 0;
};
Das ist aber auch in anderen Programmiersprachen wie Java der Fall, und ich sehe nicht, was gegen diese Möglichkeit sprechen sollte. Besonders ist hingegen in C++, dass abstrakte
Funktionen – wie im folgenden Beispiel zu sehen – implementiert und ausgeführt werden können.
// abstract class because get_answer is pure virtual
struct base
{
virtual ~base() = default;
// pure virtual function
virtual auto get_answer() -> int = 0;
};
// definition of pure virtual function
auto base::get_answer() -> int { return 42; }
struct derived : base
{
virtual auto get_answer() -> int
{
// calls pure virtual function
return base::get_answer();
}
};
Das sieht man in C++ bei normalen Funktionen zwar nur sehr selten, hat aber durchaus einen Anwendungsbereich. Insbesondere wird es genutzt, um für abstrakte
Funktionen eine Default-Implementierung bereitstellen kann, für die sich abgeleitete Klassen aber explizit entscheiden müssen.
override or not override
Einfacher ist die Situation mit dem in C++11 eingeführten, Kontext-abhängigem Schlüsselwort override
. Damit können virtuelle Funktionen gekennzeichnet werden, die eine Funktion einer Basisklasse überschreiben. Werden hingegen andere Funktionen auf diese Weise ausgezeichnet, führt dies zu einem Fehler beim Übersetzen. Mit override
wird also nicht gesteuert, welche Funktionen anderen Funktionen überschreiben, sondern es ist lediglich eine Erweiterung der statischen Typprüfung, um Flüchtigkeitsfehler bei der initialen Entwicklung und bei der Refaktorisierung zu vermeiden.
override
hat übrigens nichts mit Implementierung zu tun. Wie im folgenden Beispiel zu sehen können auch abstrakte
Funktionen so gekennzeichnet werden. Das ist in Java genauso und nach genauerer Betrachtung auch nicht verwunderlich.
struct base
{
virtual ~base() = default;
virtual auto get_answer() -> int { return 42; }
};
struct derived : base
{
virtual auto get_answer() override -> int = 0;
};
Ich selbst werde vermutlich dazu übergehen, für überschriebene Funktionen nur noch override
zu verwenden und virtual
ausschließlich in der Basisklasse.
final or not final
Ebenfalls mit C++11 wurde final
eingeführt. Eine entsprechend gekennzeichnete virtuelle Funktion kann in abgeleiteten Klassen nicht überschrieben werden. Und wie die meisten anderen Aspekte virtueller Funktionen ist auch final
weitgehend orthogonal zu betrachten. Das bedeutet beispielsweise, dass man eine virtuelle Funktion deklarieren kann, die überhaupt nicht überschrieben werden kann..
struct base
{
virtual ~base() = default;
// Why is this function virtual?
virtual auto get_answer() final -> int { return 42; }
};
Java-Entwickler verwenden diese Kombination häufig. Das liegt aber daran, dass in Java alle Funktionen virtuell sind und sich nicht-virtuelle Funktionen auf diese Weise simulieren lassen. Aber welchen Vorteil bringt das einem C++‑Entwickler?
Noch interessanter ist die Kombination mit abstrakten
Funktionen. Denn im Gegensatz zu Java kann in C++ eine Funktion sowohl pure
als auch final
sein.
struct base
{
virtual ~base() = default;
// final pure virtual function
virtual auto get_answer() final -> int = 0;
};
struct derived : base { };
Die zugehörige Klasse ist also abstrakt – so wie auch alle abgeleiteten Klassen. Damit könnte beispielsweise sichergestellt werden, dass keine Instanzen in dieser Klassenhierarchie erzeugt werden. Ein richtig guter Anwendungsfall für produktiven Code fällt mir nicht ein. Aber interessant ist die Möglichkeit eventuell für die Refaktorisierung: Immerhin lassen sich damit in Bestandscode zuverlässig alle Stellen identifizieren, an denen eine bestimmte Funktion überschrieben wird.
public or private
Mindestens eine Dimension fehlt noch: Die Zugriffsbeschränkungen. Das sind übrigens keine Sichtbarkeitsbeschränkungen, wie einem manchmal weiß gemacht wird. public
und private
sind zunächst einmal vollkommen unabhängig von der restlichen Aspekten virtueller Funktionen. Wie der Name schon sagt wird mit diesen Schlüsselwörtern der Zugriff auf die Funktionen beschränkt. Das hat aber keine Auswirkung auf das Überschreiben: Insbesondere können wie im folgenden Beispiel auch private Funktionen überschrieben werden. Die super
-Funktion kann man aber natürlich trotzdem nicht aufrufen.
struct base
{
virtual ~base() = default;
private:
virtual auto get_answer() -> int { return 42; }
};
struct derived : base
{
private:
// private function can be overridden
auto get_answer() override -> int
{
// ERROR: super function can not be called
return base::get_answer();
}
};
Für diese Art der Verwendung virtueller Funktionen gibt es zahlreiche Anwendungsfälle. Es gibt sogar ein Entwurfsmuster, das darauf basiert: Das sogenannte Non-Virtual-Interface-Pattern (NVI), das verwendet wird um zwischen internen und externen Schnittstellen zu unterscheiden.
In C++ können die Zugriffsberechtigungen beim Überschreiben auch verändert werden – und zwar in beide Richtungen. Relativ häufig ist das private Überschreiben, denn dadurch wird sichergestellt, dass die Funktionen ausschließlich über einen Basisklassenzeiger aufgerufen werden.
struct base
{
virtual ~base() = default;
virtual auto get_answer() -> int = 0;
};
struct derived : base
{
private:
// function can only be called via base class reference/pointer
auto get_answer() override -> int
{
return 42;
}
};
private final override pure virtual function
Die ganzen Eigenschaften lassen sich alle in einer Funktion kombinieren – wie im folgenden Beispiel die Funktion derviced::get_answer()
.
struct base
{
virtual ~base() = default;
virtual auto get_answer() -> int = 0;
};
struct derived : base
{
private:
virtual auto get_answer() final override -> int = 0;
};
Sinnvoll erscheint mir diese Kombination allerdings nicht mehr. Denn die pure final virtual functions
sind schon etwas merkwürdig.