C++ Core Guidelines: Definition von Funktionen

Modernes C++  –  0 Kommentare

Funktionen sind die "fundamental building block of programs" und "the most critical part in most interfaces". Diese Aussagen leiten die Regeln zu Funktionen in den "C++ Core Guidelines" ein und sind hundertprozentig richtig. Grund, tiefer in die mehr als 30 Regeln für Definition, Parameterübergabe und Rückgabewerte von Funktionen einzutauchen.

Natürlich kann ich nicht zu jeder Regel alle Details liefern; dafür gibt es einfach zu viele. Ich werde versuchen, eine Geschichte zu erzählen, damit wir sie uns merken können. Los geht es mit den Regeln zu Definition von Funktionen. Hier ist der erste Überblick.

  • F.1: “Package” meaningful operations as carefully named functions
  • F.2: A function should perform a single logical operation
  • F.3: Keep functions short and simple
  • F.4: If a function may have to be evaluated at compile time, declare it constexpr
  • F.5: If a function is very small and time-critical, declare it inline
  • F.6: If your function may not throw, declare it noexcept
  • F.7: For general use, take T* or T& arguments rather than smart pointers
  • F.8: Prefer pure functions
  • F.9: Unused parameters should be unnamed

Definition von Funktionen

F.1: "Package" meaningful operations as carefully named functions

F.2: A function should perform a single logical operation

F.3: Keep functions short and simple

Die ersten drei Regeln sollten selbstverständlich sein und teilen eine gemeinsame Idee. Ich beginne mit der Regel F2. Wenn man eine Funktion schreibt, die eine einzige logische Operation ausführt (F2), wird diese Funktion mit hoher Wahrscheinlichkeit kurz und einfach (F3) sein. Die Regeln sprechen von Funktionen, die auf einen Bildschirm passen. Nun hat man diese kurze und einfache Funktion, die genau eine Aufgabe ausführt. Dieser Funktion sollte man einen sorgfältig ausgewählten Namen geben (F1). Dieser sorgfältig ausgewählte Name ist der elementare Baustein, den Entwickler mit weiteren Bausteinen zusammenfügen und höherer Abstraktionen bilden können. Nun besitzen sie Funktionen mit intuitiven Namen und daher einen intuitiven Einstieg in das Programm.

F.4: If a function may have to be evaluated at compile time, declare it constexpr

Eine constexpr-Funktion ist eine Funktion, die zur Übersetzungszeit oder Laufzeit ausgeführt werden kann. Wer die constexpr-Funktion mit konstanten Ausdrücken aufruft und das Ergebnis dieser Funktion zur Übersetzungszeit anfordert, wird es bereits zur Übersetzungszeit erhalten. Wer eine eine solche Funktion mit Argument aufruft, die sich nicht zur Übersetzungszeit auswerten lässt, kann diese Funktion wie eine gewöhnliche Funktion verwenden.

constexpr int min(int x, int y) { return x < y ? x : y; }

constexpr auto res= min(3, 4);

int first = 3;
auto res2 = min(first, 4);

Die Funktion min kann zur Übersetzungszeit ausgeführt werden. Falls ich min mit einem konstanten Ausdruck aufrufe und das Ergebnis zur Übersetzungszeit anfordere, erhalte ich das Ergebnis zur Übersetzungszeit: constexpr auto res= min(3, 4). Ich kann min nur als gewöhnliche Funktion verwenden, falls first kein konstanter Ausdruck ist: auto res2 = min(first, 4).

Es gibt viel mehr über constexpr-Funktionen zu erzählen. Ihre Syntax war mit C++11 sehr eingeschränkt, unterscheidet sich aber mit C++14 kaum noch von einer gewöhnlichen Funktion. constexpr-Funktionen sind eine Art reine Funktion in C++. Details dazu gibt es in meinen Artikeln zu constexpr.

F.5: If a function is very small and time-critical, declare it inline

Ich war ziemlich überrascht, diese Regel zu lesen. Optimierer wenden inline an, wenn die Funktion nicht als inline deklariert ist. Das Gleiche gilt genau andersrum. Sie wenden inline nicht an, wenn die Funktion als inline deklariert ist. Letztlich ist inline nur ein Hinweis für den Optimierer, den Funktionsaufruf durch seinen Funktionskörper zu ersetzen.

constexpr-Funktionen sind implizit inline. Das Gleiche gilt per Default für Klassenmethoden, die im Klassenkörper definiert werden, oder auch für Funktions-Templates.

Mit modernen Compilern ist der wichtigste Grund für den Einsatz von inline ein anderer. Es erlaubt, die One Defintion Rule (ODR) zu brechen. Man darf inline-Funktionen in mehr als einer Übersetzungseinheit definieren. Hier geht es zu meinem Artikel zu inline.

F.6: If your function may not throw, declare it noexcept

Indem Entwickler eine Funktion als noexcept erklären, reduzieren sie die Anzahl der alternativen Kontrollpfade; daher ist noexcept ein Hinweis für den Optimierer.

Sogar wenn eine Funktion eine Ausnahme werfen kann, ist der Einsatz von noexcept oft sinnvoll. noexcept bedeutet in dem Fall: Ich kümmere mich nicht um Ausnahmen. Der Grund mag sein, dass Entwickler keine explizite Möglichkeit besitzen, um auf die Ausnahme zu reagieren. Daher besteht als einzige Möglichkeit, dass automatisch terminate() aufgerufen wird.

Hier ist ein Beispiel für eine Funktion, die als noexcept deklariert wurde. Es kann eine Ausnahme werfen, falls dem Programm der Speicher ausgeht.

vector<string> collect(istream& is) noexcept
{
vector<string> res;
for (string s; is >> s;)
res.push_back(s);
return res;
}

F.7: For general use, take T* or T& arguments rather than smart pointers

Wenn man Smart Pointer einsetzt, schränkt man den Anwendungsbereich einer Funktion ein. Das folgende Beispiel bringt die Aussage auf den Punkt.

// accepts any int*
void f(int*);

// can only accept ints for which you want to transfer ownership
void u(unique_ptr<int>);

// can only accept ints for which you are willing to share ownership
void s(shared_ptr<int>);

// accepts any int
void h(int&);

Die Funktionen u und s besitzen eine besondere Besitzsemantik. u will exklusiv sein Argument besitzen, s will sein Argument teilen. Die Funktion s hat einen kleinen Performanz-Overhead. Der Referenzzähler für std::shared_ptr muss erhöht und erniedrigt werden. Diese atomare Operation benötigt ein wenig Zeit.

F.8: Prefer pure functions

Reine Funktionen sind Funktionen, die immer das gleiche Ergebnis zurückgeben, wenn sie die gleichen Argumente erhalten. Diese Eigenschaft wird auch referenzielle Transparenz genannt.

Reine Funktionen besitzen ein paar sehr interessante Charakteristiken.

Diese Charakteristiken haben sehr weitreichende Konsequenzen, denn durch sie kann man die Funktion in Isolation betrachten:

  • Die Korrektheit der Funktion ist einfacher zu verifizieren.
  • Die Refaktorierung und das Testen der Funktion sind einfacher.
  • Man kann das Ergebnis des Funktionsaufrufs zwischenspeichern.
  • Man kann reine Funktionen umsortieren oder in anderen Threads ausführen.

Reine Funktionen werden auch gerne mathematische Funktionen genannt. Per Default besitzt C++ keine reine Funktionen wie die rein funktionale Sprache Haskell. constexpr-Funktionen sind nur fast reine Funktionen, daher basiert Reinheit in C++ vor allem auf der Disziplin der Entwickler.

Nur der Vollständigkeit halber: Template-Metaprogrammierung ist eine rein funktionale Subsprache, eingebettet in die imperative Sprache C++. Wer neugierig ist, hier geht es weiter mit Template-Metaprogrammierung.

F.9: Unused parameters should be unnamed

Falls man keine Namen für die nicht verwendeten Funktionsparameter verwendet, wird eine Funktion einfacher zu lesen sein und man wird keine Warnung für nicht verwendete Parameter erhalten.

Wie geht's weiter?

Das waren die Regeln zur Definition von Funktionen. Im nächsten Artikel geht es darum, welche Regeln für Funktionsparameter gelten.