C++ Core Guidelines: Interfaces II

Die C++ Core Guidelines stellen 20 Regeln für Interfaces auf, denn "interfaces is probably the most important single aspect of code organization". Was steckt hinter den letzten zehn?

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

Die C++ Core Guidelines stellen 20 Regeln für Interfaces auf, denn "interfaces is probably the most important single aspect of code organization". Was steckt hinter den letzten zehn?

Interfaces sind ein Vertrag zwischen einem Serviceanbieter und einem Servicenutzer. In meinem letzten Artikel stellte ich die ersten zehn in den C++ Core Guidelines festgehaltenen Regeln vor. Konsequenterweise schreibe ich nun über die verbleibenden zehn. Hier noch einmal alle 20 im Überblick:

  • I.1: Make interfaces explicit
  • I.2: Avoid global variables
  • I.3: Avoid singletons
  • I.4: Make interfaces precisely and strongly typed
  • I.5: State preconditions (if any)
  • I.6: Prefer Expects() for expressing preconditions
  • I.7: State postconditions
  • I.8: Prefer Ensures() for expressing postconditions
  • I.9: If an interface is a template, document its parameters using concepts
  • I.10: Use exceptions to signal a failure to perform a required task
  • I.11: Never transfer ownership by a raw pointer (T*)
  • I.12: Declare a pointer that must not be null as not_null
  • I.13: Do not pass an array as a single pointer
  • I.22: Avoid complex initialization of global objects
  • I.23: Keep the number of function arguments low
  • I.24: Avoid adjacent unrelated parameters of the same type
  • I.25: Prefer abstract classes as interfaces to class hierarchies
  • I.26: If you want a cross-compiler ABI, use a C-style subset
  • I.27: For stable library ABI, consider the Pimpl idiom
  • I.30: Encapsulate rule violations

Und los geht es mit den Details.

X* 
{
X* res = new X{};
// ...
return res;
}

Wer kümmert sich im oben gezeigten Code um den Zeiger X? Es gibt mindestens drei Möglichkeiten die Besitzverhältnisse zu klären:

  • Gib anstelle eines Zeigers einen Wert zurück.
  • Verwende einen Smart Pointer.
  • Verwende owner<X*> aus der guideline support library (GSL).

Welchen semantischen Unterschied besitzen die drei Variationen der folgenden Funktion length?

// it is not clear whether length(nullptr) is valid
int length(const char* p);

// better: we can assume that p cannot be nullptr
int length(not_null<const char*> p);

// we must assume that p can be nullptr
int length(const char* p);

Die Absicht der zweiten und dritten Variation ist ziemlich offensichtlich. Die zweite Variante nimmt nur einen Zeiger an, der kein Nullzeiger (nullptr) sein kann. Die letzte Variante akzeptiert auch einen nullptr. Ich denke, du ahnst es bereits: not_null ist aus der GSL.

Arrays einfach als Zeiger zu übergeben, ist sehr fehleranfällig.

void copy_n(const T* p, T* q, int n); // copy from [p:p+n) to [q:q+n)

Was passiert, wenn n zu groß ist? Genau: undefiniertes Verhalten. Die GSL bietet ein Lösung an, die sich "spans" nennt:

void copy(span<const T> r, span<T> r2); // copy r to r2

Spans leiten die Anzahl ihrer Argument automatisch ab.

Globale Objekte können für viel "Spaß" sorgen. Wenn sie zum Beispiel in verschiedenen Übersetzungseinheiten sind, ist die Reihenfolge ihrer Initialisierung nicht definiert. So weist der folgende Programmschnipsel etwa undefiniertes Verhalten auf:

// file1.c

extern const X x;

const Y y = f(x); // read x; write y

// file2.c

extern const Y y;

const X x = g(y); // read y; write x

Es gibt eine einfache Regel: eine Funktion soll genau eine Aufgabe erfüllen. Falls diese Regel eingehalten wird, reduziert sich automatisch die Anzahl der Argumente und die Funktion ist einfacher zu verwenden.

Um ehrlich zu sein, die neuen Algorithmen in C++17 wie std::transform_reduce verletzen bisweilen diese Regel.

Welcher Parameter stellt die Quelle und das Ziel der folgenden copy_n Funktion dar? Wohlbegründete Vermutung sind willkommen.

void copy_n(T* p, T* q, int n);

Ich muss leider allzu häufig in die Dokumentation schauen.

Zugegeben, das ist eine offensichtliche und lang bewährte Regel für objektorientiertes Design. Für die Regel gibt es zwei Begründungen:

  • Abstrakte Basisklassen sind häufig deutlich stabiler als nicht-abstrakte Basisklassen.
  • Nicht-abstrakte Basisklassen mit Zustand und nicht-abstrakten Methoden schränken die abgeleiteten Klassen deutlich ein.

ABI steht für Application Binary Interface.

Das ist natürlich eine seltsame Regel in den C++ Core Guidelines. Der Grund ist: "Different compilers implement different binary layouts for classes, exception handling, function names, and other implementation details." Auf einigen Plattformen entstehen aber gemeinsame ABIs. Falls du einen Compiler verwendest, kannst du natürlich alle C++-Funktionen nutzen. In diesem Fall musst du deinen Code gegebenenfalls neu kompilieren.

Pimpl steht für Pointer to Implementation und ist die C++-Variante des bekannten Bridge Pattern. Die Idee ist es, dass ein nicht-polymorphes Interface einen Zeiger auf seine Implementierung hält, sodass Veränderungen der Implementierung kein neues Kompilieren des Interfaces erfordern.

Hier ist ein Beispiel aus den C++ Core Guidelines:

interface (widget.h)
class widget {
class impl;
std::unique_ptr<impl> pimpl;
public:
void draw(); // public API that will be forwarded to the implementation
widget(int); // defined in the implementation file
~widget(); // defined in the implementation file, where impl is a complete type
widget(widget&&) = default;
widget(const widget&) = delete;
widget& operator=(widget&&); // defined in the implementation file
widget& operator=(const widget&) = delete;
};

implementation (widget.cpp)

class widget::impl {
int n; // private data
public:
void draw(const widget& w) { /* ... */ }
impl(int n) : n(n) {}
};
void widget::draw() { pimpl->draw(*this); }
widget::widget(int n) : pimpl{std::make_unique<impl>(n)} {}
widget::~widget() = default;
widget& widget::operator=(widget&&) = default;

Der pimpl ist der Zeiger, der die Implementierung referenziert.

Details zum pimpl-Idiom kannst du schön im GOW #100-Artikel von Herb Sutter nachlesen. GOW steht für Guru Of the Week.

Manchmal ist Code hässlich, nicht sicher oder fehleranfällig. Fasse den Code zusammen und verpacke ihn in ein einfach zu verwendetens Interface. Dieser Vorgang ist eine Abstraktion, die du ab und an anwenden musst. Um ehrlich zu sein, ich habe mit so etwas kein Problem, solang der Code stabil ist und das Interface nur erlaubt, ihn richtig zu verwenden.

In meinem letzten Artikel und in diesem habe ich oft die Guideline Support Library erwähnt. Jetzt ist es an der Zeit, einen tieferen Blick zu wagen. Genau das werde ich im nächsten Artikel tun. ()