C++ Core Guidelines: Regeln für Templates und Ableitungshierachien

Templates sind das Rückgrat der C++-Unterstützung für generische Programmierung und Klassenhierarchien das der Unterstützung der objektorientierten Programmierung. Beiden Sprachmechanismen lassen sich effektiv in Kombination nutzen, aber einige Design-Pitfalls müssen vermieden werden.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 6 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Gemäß den C++ Core Guidelines gilt, dass "Templates are the backbone of C++’s support for generic programming and class hierarchies the backbone of its support for object-oriented programming. The two language mechanisms can be used effectively in combination, but a few design pitfalls must be avoided." Genau um diese Fallen geht es in dem heutigen Artikel.

Dieser Artikel besteht aus fünf Regeln.

Die Regel T.81 besitzt nur einen leichten Bezug zu Templates und zur Ableitungshierarchie und die Regel T.82 ist leer. Daher konzentriere ich mich im heutigen Artikel auf die drei verbleibenden Regeln.

Über die Regeln T.80 und T.84 werde ich zusammenschreiben, denn T.84 setzt die Geschichte von T.80 fort.

Hier ist das Beispiel aus der naiven Template-Hierarchie aus den Guidelines:

template<typename T>
struct Container { // an interface
virtual T* get(int i);
virtual T* first();
virtual T* next();
virtual void sort();
};

template<typename T>
class Vector : public Container<T> {
public:
// ...
};

Vector<int> vi;
Vector<string> vs;

Warum ist diese Implementierung gemäß der Guidelines naiv? Insbesondere deswegen, da die Basisklasse Container mehrere virtuelle Funktionen enthält. Dies ist zu viel Code. Virtuelle Funktionen werden in einem Klassen-Template immer instanziiert. Im Gegensatz dazu werden nichtvirtuelle Funktionen nur dann instanziiert, wenn sie verwendet werden.

Ein einfacher Test mit CppInsight belegt meine Aussage.

Das folgende Programm verwendet einen std::vector<int> und einen std::vector<std::string>:

CppInsight bringt es auf den Punkt. Für std::vector wird keine Funktion erzeugt:

Dies ist das vereinfachte Programm aus den Guidelines, dass eine virtuelle Funktion sort enthält:

Jetzt wird die virtuelle Funktion sort instanziiert. Der Screenshot zeigt lediglich die Ausgabe von CppInsight, die die Instanziierung des Klassen-Templates Container enthält:

In Summe erhalte ich 100 Zeilen Code, wenn ich die virtuelle Funktion verwende.

Die Guidelines geben einen Tipp, wie sich die Codegröße reduzieren lässt. Oft lässt sich ein robustes Interface definieren, in dem die Basisklasse nicht parametrisiert wird. Das bringt mich direkt zur verwandten Regel T.84: Use a non-template core implementation to provide an ABI-stable interface.

Zu der Regel habe ich bereits in dem Artikel "C++ Core Guidelines: Template-Definitionen" geschrieben.

Die Regel gibt aber einen weiteren Tipp für ein stabiles Interface: Pimpl.

Pimpl steht für "pointer to implementation" und bedeutet, dass eine Klasse ihre Interna in einer separaten Klasse platziert, auf die sie mittels eines Zeigers zugreift. Diese Technik sollte sich in der Werkzeugkiste jedes ernsthaften C++ Programmierers befinden. Pimpl wird auch gerne "compilation firewall" genannt, denn dies Technik bricht die Abhängigkeit zwischen der Implementierung und dem Nutzer der Klassen-Hierarchie. Dies heißt, dass die Implementierung verändert werden kann, ohne den Code zu zu kompilieren. Die Details dazu gibt es in Herb Sutters Blog: GotW #100: Compilation Firewalls:

// in header file 
class widget {
public:
widget();
~widget();
private:
class impl; // (1)
unique_ptr<impl> pimpl;
};

// in implementation file // (2)
class widget::impl {
// :::
};

widget::widget() : pimpl{ new impl{ /*...*/ } } { }
widget::~widget() { }

Dies sind die Charakteristiken des Idioms.

  1. Verschiebt alle privaten nichtvirtuellen Funktion nach impl (1)
  2. Deklariert impl vorwärts
  3. Definiert impl in der entsprechenden Implemtationdatei (3)

Gerne zeige ich noch das vollständige Beispiel von cppreference.com:

// pimpl.cpp

#include <iostream>
#include <memory>

// interface (widget.h)
class widget {
class impl;
std::unique_ptr<impl> pImpl;
public:
void draw();
bool shown() const { return true; }
widget(int);
~widget();
widget(widget&&) = default;
widget(const widget&) = delete;
widget& operator=(widget&&);
widget& operator=(const widget&) = delete;
};

// implementation (widget.cpp)
class widget::impl {
int n; // private data
public:
void draw(const widget& w) { // (1)
if(w.shown())
std::cout << "drawing a widget " << n << '\n';
}
impl(int n) : n(n) {}
};
void widget::draw() { pImpl->draw(*this); } // (2)
widget::widget(int n) : pImpl{std::make_unique<impl>(n)} {}
widget::~widget() = default; // (3)
widget& widget::operator=(widget&&) = default;

// user (main.cpp)
int main()
{
std::cout << std::endl;

widget w(7);
w.draw();

std::cout << std::endl;

}

Der draw-Aufruf der Klasse impl verwendet eine Rückwärtsreferenz auf widget (Zeile (1) und (2)). Der Destruktor und der Move-Zuweisungsoperator (Zeile (3)) müssen in der Implementierungsdatei definiert werden, denn std::unique_ptr verlangt, dass der Datentyp, auf den er verweist, vollständig ist.

Das Programm verhält sich erwartungsgemäß:

Neben seinen Vorteilen besitzt das Pimpl-Idiom auch zwei Nachteile: Zum einen enthält es einen zusätzliche Zeiger Indirektion, zum anderen muss der Zeiger gespeichert werden.

Die letzte Regel des heutigen Artikels handelt von einem typischen Fehler.

Lass mich ein virtuelles Methoden-Template verwenden:

// virtualMember.cpp

class Shape {
template<class T>
virtual bool intersect(T* p);
};

int main(){

Shape shape;

}

Die Fehlermeldung meines GCC-8.2-Compilers spricht eine eindeutige Sprache:

Die nächsten Regeln und daher mein nächster Artikel befassen sich mit Variadic Templates. Das sind Templates, die beliebig viele Argumente annehmen können. Die Regeln in den Guidelines, wie viele Regeln zu Templates bestehen nur auf Überschriften. Das bedeutet, dass mein nächster Artikel zu Variadic Templates allgemeiner Natur sein wird. ()