C++ Core Guidelines: Semantik der Funktionsargumente und RĂĽckgabewerte
Heute schlieĂźe ich meinen Artikel ĂĽber Funktionen in den C++ Core Guidelines ab. Der letzte Artikel hat die Syntax der Funktionsparameter und RĂĽckgabewerte behandelt. In diesem geht es um deren Semantik.
- Rainer Grimm
Heute schlieĂźe ich meinen Artikel ĂĽber Funktionen in den C++ Core Guidelines ab. Der letzte Artikel hat die Syntax der Funktionsparameter und RĂĽckgabewerte behandelt. In diesem geht es um deren Semantik.
Bevor ich in die Details dieses Artikels abtauche, hier ist in bekannter Manier erst einmal ein Ăśberblick zu den semantischen Regeln fĂĽr Parameter, RĂĽckgabewerte und ein paar weitere Regeln rund um Funktionen.
Parameter passing semantic rules:
- F.22: Use T* or owner<T*> to designate a single object
- F.23: Use a not_null<T> to indicate "null" is not a valid value
- F.24: Use a span<T> or a span_p<T> to designate a half-open sequence
- F.25: Use a zstring or a not_null<zstring> to designate a C-style string
- F.26: Use a unique_ptr<T> to transfer ownership where a pointer is needed
- F.27: Use a shared_ptr<T> to share ownership
Value return semantic rules:
- F.42: Return a T* to indicate a position (only)
- F.43: Never (directly or indirectly) return a pointer or a reference to a local object
- F.44: Return a T& when copy is undesirable and "returning no object" isn't an option
- F.45: Don't return a T&&
- F.46: int is the return type for main()
- F.47: Return T& from assignment operators.
Other function rules:
- F.50: Use a lambda when a function won’t do (to capture local variables, or to write a local function)
- F.51: Where there is a choice, prefer default arguments over overloading
- F.52: Prefer capturing by reference in lambdas that will be used locally, including passed to algorithms
- F.53: Avoid capturing by reference in lambdas that will be used nonlocally, including returned, stored on the heap, or passed to another thread
- F.54: If you capture this, capture all variables explicitly (no default capture)
- F.55: Don't use va_arg arguments
Parameter passing semantic
Die Regeln dieses Abschnitts kann ich sehr kompakt abhandeln. Die meisten habe ich bereits im Artikel zu der Guidelines Support Library beschrieben. Wer daher neugierig bist, lese den zitierten Artikel. Ich werde nur kurz auf die erste Regel F.22 eingehen.
F.22: Use T* or owner<T*> to designate a single object
Was heißt, dass T* ein einzelnes Objekt bezeichnen soll? Die Regel beantwortet direkt die Frage. Zeiger können für viele Zwecken verwendet werden. Zeiger können die folgenden Rollen annehmen:
- Objekte, die von der Funktion nicht gelöscht werden dürfen
- Objekte, die auf dem Heap angelegt wurden und von der Funktion gelöscht werden müssen
- Nullzeiger (nullptr)
- C-Strings
- C-Arrays
- Positionen in C-Arrays
Angesichts dieser verschiedenen Rollen, die Zeiger annehmen können, sollte man sie nur für Objekte (1) verwenden, die nicht gelöscht werden dürfen.
Wie bereits angekĂĽndigt, werde ich die verbleibenden Regeln F.23 bis F.27 zu Funktionsparametern ĂĽberspringen.
Value return semantic rules
F.42: Return a T* to indicate a position (only)
Das lässt sich noch besser auf den Punkt bringen. Man soll keine Zeiger verwenden, um Besitzverhältnisse auszudrücken. Das ist ein Missbrauch von Zeigern. Hier ist ein Beispiel:
Node* find(Node* t, const string& s) // find s in a binary tree of Nodes
{
if (t == nullptr || t->name == s) return t;
if ((auto p = find(t->left, s))) return p;
if ((auto p = find(t->right, s))) return p;
return nullptr;
}
Die Guidelines sind in diesem Punkt sehr eindeutig. Man darf nicht ein Objekt aus einer Funktion zurückgeben, das sich nicht bereits im Bereich der aufrufenden Funktion befindet. Die nächste Regel adressiert genau diesen typischen Programmierfehler.
F.43: Never (directly or indirectly) return a pointer or a reference to a local object
Diese Regel ist sehr einleuchtend, lässt sich aber mit ein paar verschachtelten Funktionsaufrufen allzu leicht aushebeln. Das Unheil nimmt in dem folgenden Beispiel mit der Funktion f seinen Lauf. f gibt einen Zeiger auf ein lokales Objekt zurück.
int* f()
{
int fx = 9;
return &fx; // BAD
}
void g(int* p) // looks innocent enough
{
int gx;
cout << "*p == " << *p << '\n';
*p = 999;
cout << "gx == " << gx << '\n';
}
void h()
{
int* p = f();
int z = *p; // read from abandoned stack frame (bad)
g(p); // pass pointer to abandoned stack frame to function (bad)
}
F.44: Return a T& when copy is undesirable and "returning no object" isn't an option
Die C++-Sprache sichert zu, dass ein Referenz T& immer auf ein Objekt verweist. Daher muss der Aufrufer nicht auf einen Nullzeiger nullptr prĂĽfen, da dies keine Option sein kann. Die Regel stellt kein Widerspruch zu vorherigen Regeln F.43 dar. F.43 besagt, dass man keine Referenz auf ein lokales Objekt zurĂĽckgeben soll.
F.45: Don't return a T&&
Mit T&& können Entwickler eine Referenz auf ein bereits zerstörtes Objekt zurückgeben. Das ist sehr bösartig, und sie sind sehr leicht mitten im undefinierten Verhalten (F.43).
Falls der Aufruf f() eine Kopie zurückgibt, erhält man eine Kopie auf ein temporäres Objekt.
template<class F>
auto&& wrapper(F f)
{
...
return f();
}
Die einzigen Ausnahmen zu dieser Regel sind die Funktionen std::move fĂĽr Move-Semantik und std::forward fĂĽr Perfect Forwarding.
F.46: int is the return type for main()
Standard C++ kennt zwei Arten, die main-Funktion zu deklarieren. void ist keine Option in C++ und schränkt daher die Portabilität des Codes ein.
int main(); // C++
int main(int argc, char* argv[]); // C++
void main(); // bad, not C++
Die zweite Form ist äquivalent zu int main(int argc, char** argv).
Die main-Funktion gibt automatisch return 0 zurĂĽck, falls eine main-Funktion keine return-Anweisung besitzt.
F.47: Return T& from assignment operators
Der Copy-Zuweisungsoperator sollte T& zurückgeben. In diesem Fall ist er konsistent mit den Containern der Standard Template Library und folgt dem bewährten Prinzip: "do as the ints do".
Es besteht ein feiner Unterschied, ob ein Copy-Zuweisungsoperator sein Ergebnis mittels Referenz oder Copy zurĂĽckgibt.
- A& operator=(constA& rhs){ ... };
- A operator=(constA& rhs){ ... };
Im zweiten Fall führt die Kette von Zuweisungen zu zwei zusätzlichen Copy-Konstruktor und Destruktor Aufrufen.
Other function rules:
F.50: Use a lambda when a function won't do (to capture local variables, or to write a local function)
In C++11 gibt es aufrufbare Einheiten wie Funktionen, Funktionsobjekte und Lambda-Funktionen. Häufig taucht die Frage auf: Wann soll ich eine Funktion oder eine Lambda-Funktion verwenden? Hier sind zwei einfache Faustregeln.
- Falls eine aufrufbare Einheit lokale Variablen verwendet oder in einem lokalen Bereich verwendet wird, sollte man eine Lambda-Funktion einsetzen.
- Falls man eine aufrufbare Einheit ĂĽberladen will, setzt man eine Funktion ein.
void print(const string& s, format f = {});
versus
void print(const string& s); // use default format
void print(const string& s, format f);
F.51: Where there is a choice, prefer default arguments over overloading
Falls man eine Funktion mit einer variablen Anzahl an Argumenten aufrufen muss, zieht man Default-Argumente dem Ăśberladen der Funktion vor. Damit setzt man automatisch das DRY-Prinzip um (don't repeat yourself).
F.52: Prefer capturing by reference in lambdas that will be used locally, including passed to algorithms
Aus Performanz- und KorrektheitsgrĂĽnden werden Entwickler meist ihre Variablen in Lambda-Funktionen per Referenz verwenden. Aus EffizienzgrĂĽnden heiĂźt dies entsprechend der Regel F.16, falls fĂĽr ihre Variable p gilt: sizeof(p) > 4*sizeof(int).
Da man eine Lambda-Funktion lokal verwendest, bekommt man auch keine Lebenszeitproblem mit einer verwendeten Variable message.
std::for_each(begin(sockets), end(sockets), [&message](auto& socket)
{
socket.send(message);
});
F.53: Avoid capturing by reference in lambdas that will be used nonlocally, including returned, stored on the heap, or passed to another thread
Man muss sehr vorsichtig sein, wenn man einen Thread im Hintergrund laufen lässt (detach). Der kleine Codeschnipsel besitzt bereits zwei Race Condition.
std::string s{"undefined behaviour"};
std::thread t([&]{std::cout << s << std::endl;});
t.detach();
- Der erzeugte Thread t kann länger leben als sein Erzeuger. Daher existiert std::string eventuell nicht mehr.
- Der erzeugte Thread t möchte länger leben als sein Erzeuger. Daher existiert std::cout eventuell nicht mehr.
F.54: If you capture this, capture all variables explicitly (no default capture)
Es scheint, als ob man mit [=] alle Argumente per Copy bindest. Tatsächlich bindet man aber in einem Objekt damit alle Mitglieder per Referenz. Das kann, muss aber nicht die Intention sein.
class My_class {
int x = 0;
void f() {
auto lambda = [=]{ std::cout << x; }; // bad
x = 42;
lambda(); // 42
x = 43;
lambda(); // 43
}
};
Die Lambda-Funktion bindet x per Referenz.
F.55: Don’t use va_arg arguments
Falls eine Funktion eine beliebige Anzahl an Argumenten annehmen soll, setzt man Variadic Templates ein. Im Gegensatz zu va_args kann der Compiler bei diesen automatischen den richtigen Typ bestimmen. Mit C++17 lässt sich auf diese beliebige Anzahl an Argumente direkt ein Operator anwenden.
template<class ...Args>
auto sum(Args... args) { // GOOD, and much more flexible
return (... + args); // note: C++17 "fold expression"
}
sum(3, 2); // ok: 5
sum(3.14159, 2.71828); // ok: ~5.85987
Falls der Codeschnipsel fĂĽr ungewohnt ausschaut, hier geht es zu meinem Artikel zu Fold Expressions.
Wie geht's weiter?
Klassen sind benutzerdefinierte Typen. Sie erlauben es, Zustand und Verhalten zu kapseln. Dank Klassenhierarchien kannst man Typen organisieren. Daher geht es in dem nächsten Artikel um Regeln für Klassen und Klassenhierarchien. ()