C++ Core Guidelines: Die Regeln für In-, Out-, In-out-, Consume- und Forward-Funktionsparameter

Modernes C++  –  19 Kommentare

Es gibt viele Möglichkeiten, Funktionsparameter zu übergeben. Du kannst sie kopieren oder als Referenz übergeben. Diese Referenz kann konstant oder nicht konstant sein. Man kann seine Parameter sogar verschieben oder "forward" darauf anwenden. Die Entscheidung sollte davon abhängig sein, ob der Parameter ein In-, Out-, In-out-, Consume- oder Forward-Funktionsparameter ist.

Hier sind alle Regeln für die Übergabe von Ausdrücken:

  • F.15: Prefer simple and conventional ways of passing information.
  • F.16: For "in" parameters, pass cheaply-copied types by value and others by reference to const.
  • F.17: For "in-out" parameters, pass by reference to non-const.
  • F.18: For "consume" parameters, pass by X&& and std::move the parameter.
  • F.19: For "forward" parameters, pass by TP&& and only std::forward the parameter.
  • F.20: For "out" output values, prefer return values to output parameters.
  • F.21: To return multiple "out" values, prefer returning a tuple or struct.
  • F.60: Prefer T* over T& when "no argument" is a valid option.

Das schaut nach viel Regelwerk aus. Es ist aber bei Weitem nicht so schlimm. Die erste Regel F.15 fasst die Regeln F.16 bis F.21 zusammen.

F.15: Prefer simple and conventional ways of passing information

Hier kommt das große Bild von den C++ Core Guidelines. Dies sind die Standardregeln für die Übergabe von Parametern.

Basierend auf diesen Regeln gibt es ein paar Ergänzungen in grün. Diese werden "Advanced Parameter Passing Rules" genannt.

Die Begründung für diese Regeln und deren Variationen folgt in den nächsten Regeln.

F.16: For "in" parameters, pass cheaply-copied types by value and others by reference to const

Die Regeln für In-Parameter sind leicht eingängig:

void f1(const string& s);  // OK: pass by reference to const; always cheap

void f2(string s); // bad: potentially expensive

void f3(int x); // OK: Unbeatable

void f4(const int& x); // bad: overhead on access in f4()

In meinen Schulungen taucht immer wieder die Frage auf: Was bedeutet, billig zu kopieren? In diesem Punkt sind die Guidelines sehr konkret:

  • Du sollst deinen Parameter p nicht kopieren, falls sizeof(p) > 4 * sizeof(int) gilt.
  • Du sollst keine konstante Referenz auf p verwenden, falls sizeof(p) < 3 * sizeof(int) gilt.

Ich nehme an, dass diese Zahlen auf Erfahrung basieren.

F.17: For "in-out" parameters, pass by reference to non-const

In-out-Parameter werden in der Funktion verändert. Daher sollten sie als nicht-konstante Referenz übergeben werden:

void appendElements(std::vector<int>& vec){
// append elements to vec
...
}

F.18: For "consume" parameters, pass by X&& and std::move the parameter

Dies ist die erste fortgeschrittene (advanced) Regel um Parameter zu konsumieren (consume). Verwende eine Rvalue-Referenz, falls du den Parameter konsumierst und im Funktionskörper verschiebst (move). Hier kommst schon das Beispiel:

// sink takes ownership of whatever the argument owned
void sink(vector<int>&& v) {
// usually there might be const accesses of v here
store_somewhere(std::move(v));
// usually no more use of v here; it is moved-from
}

Es gibt aber eine Ausnahme dieser Regel. std::unique_ptr ist ein Datentyp, der sich nur verschieben, aber nicht kopieren lässt. Du kannst ihn verschieben, da diese Operation sehr billig ist.

void sink(std::unique_ptr<int> p) { 
...
}
...
sink(std::move(uniqPtr));

F.19: For "forward" parameters, pass by TP&& and only std::forward the parameter

Dies ist ein bekanntes Idiom in C++, dass Fabrikfunktionen zum Erzeugen von Smart Pointern wie std::make_unique oder std::make_shared anwenden. Beide Funktionen nehmen einen Datentyp T und Argumente args an und reichen diese identisch weiter (forward). Das Beispiel zeigt eine mögliche Implementierung von std::make_unique.

template<typename T, typename... Args>                             // 1
std::unique_ptr<T> make_unique(Args&&... args) // 2
{
return std::unique_ptr<T>(new T(std::forward<Args>(args)...)); // 3
}

Dieses Muster wird "Perfect Forwarding" genannt: Ein Funktions-Template wendet Perfect Forwarding an, falls es seine Parameter unverändert weiterreicht.

Hier geht es direkt zu meinem Artikel über Perfect Forwarding.

Um Perfect Forwarding für ein Funktions-Template einzusetzen, gilt es, ein dreiteiliges Rezept anzuwenden. Dazu werden, wie bei std::make_unique, keine Variadic Templates ( ... ) benötigt. Aus diesem Grund verzichte ich darauf.

  1. Du benötigst einen Template-Parameter: typename Args.
  2. Nimm dein Funktionsargument als Perfect-Forwarding-Referenz an: Args&& args.
  3. Reiche das Funktionsargument unverändert durch: std::forward<Args>(args).

F.20: For "out" output values, prefer return values to output parameters

Ein expliziter Return-Wert dokumentiert die Absicht einer Funktion. Dagegen ist ein Parameter mit einer Referenz, der als Rückgabewert (out) verwendet wird, leicht zu missbrauchen. Dieser Rückgabewert kann auch als In-out-Wert verstanden werden. Eine Funktion soll ihr Ergebnis per Copy zurückgeben. Diese Regel gilt selbst für die Container der Standard Template Library, die unter der Decke Move-Semantik einsetzen.

// OK: return pointers to elements with the value x
vector<const int*> find_all(const vector<int>&, int x);

// Bad: place pointers to elements with value x in-out
void find_all(const vector<int>&, vector<const int*>& out, int x);

Keine Regel ohne Ausnahme. Falls du ein Objekt zurückgeben willst, das teuer zu verschieben ist, kannst du eine Referenz als Rückgabewert (out) verwenden.

struct Package {      // exceptional case: expensive-to-move object
char header[16];
char load[2024 - 16];
};

Package fill(); // Bad: large return value
void fill(Package&); // OK

F.21: To return multiple "out" values, prefer returning a tuple or struct

Ab und zu gibt eine Funktion mehr als einen Wert (out) zurück. In diesem Fall solltest du ein std::tuple oder ein struct verwenden, aber keinen Parameter mit einer Referenz. Das ist sehr fehleranfällig.

// BAD: output-only parameter documented in a comment
int f(const string& input, /*output only*/ string& output_data)
{
// ...
output_data = something();
return status;
}

// GOOD: self-documenting
tuple<int, string> f(const string& input)
{
// ...
return make_tuple(status, something());
}

Dank Structured Binding kann in C++17 eine Funktion sehr elegant mehrere Werte zurückgeben:

auto [value, success] = getValue(key);

if (success){
// do something with the value;

Die Funktion getValue gibt ein Paar zurück. success zeigt an, dass die Abfrage erfolgreich war.

Die nächste Regel ist speziell. Für mich ist es eher eine semantische Regel.

F.60: Prefer T* over T& when “no argument” is a valid option

Falls dein Parameter niemals ein Nicht-Argument wie nullptr annehmen kann, solltest du eine Referenz T& verwenden. T& kann keinen nullptr annehmen. Falls ein nullptr zulässig ist, verwende T*.

std::string upperString(std::string* str){
if (str == nullptr) return std::string{}; // check for nullptr
else{
...
}

Falls ein Nicht-Argument eine Option ist, musst du dies natürlich prüfen.

In diesem Artikel ging es um In-, Out-, In-out-, Consume- und Forward-Parameter. Das sind aber nicht alle Fragen, die es zu beantworten gilt. Wie sollst du mit Sequenzen oder Besitzverhältnissen umgehen? Genau darauf werde ich im nächsten Artikel eingehen.