C++11: pure virtual functions with override and final

von Hubert Schmid vom 2012-02-19

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.