Funktionen & Parameterübergabe
Meistere die verschiedenen Arten, in C++ Argumente zu übergeben, verstehe die Rückgabewertoptimierung, die Funktionsüberladung und moderne Attribute wie [[nodiscard]].
Funktionen: die zentrale Abstraktion
Funktionen sind die grundlegende Einheit der Codeorganisation in C++. Anders als viele andere Sprachen gibt dir C++ feinkörnige Kontrolle darüber, wie Argumente an Funktionen übergeben und von ihnen zurückgegeben werden. Diese Kontrolle ist kein syntaktischer Zucker — sie hat echte Auswirkungen auf die Performance.
Die Art, wie du Parameter übergibst, bestimmt, ob die Funktion mit einer Kopie des Arguments, einer Referenz auf das Original oder einem Zeiger auf das Original arbeitet. Den richtigen Mechanismus zu wählen ist eine der wichtigsten Entscheidungen beim Entwurf einer Funktionsschnittstelle.
Mechanismen der Parameterübergabe
C++ kennt vier wesentliche Arten, Argumente zu übergeben. Jede bringt eigene Kompromisse bei Performance, Sicherheit und Semantik mit sich:
#include <iostream>
#include <string>
#include <vector>
// 1. Pass by VALUE — the function gets a copy
// Use for: small types (int, double, char), when you need a local copy
void square(int x) { // x is a copy of the argument
x = x * x; // Modifying x does NOT affect the caller
std::cout << "Inside: " << x << '\n';
}
// 2. Pass by REFERENCE — the function works on the original
// Use for: when you need to modify the caller's variable
void double_it(int& x) { // x IS the caller's variable
x *= 2; // This DOES modify the caller's variable
}
// 3. Pass by CONST REFERENCE — read-only access to the original, no copy
// Use for: large objects you only need to read
void print_all(const std::vector<std::string>& names) {
for (const auto& name : names) {
std::cout << name << '\n';
}
// names.push_back("Eve"); // ERROR: names is const
}
// 4. Pass by POINTER — explicit nullable indirection
// Use for: optional parameters (can be nullptr), C API interop
void maybe_increment(int* ptr) {
if (ptr != nullptr) { // Must check for null!
++(*ptr);
}
}
int main() {
int val = 5;
square(val);
std::cout << "After square: " << val << '\n'; // Still 5 (copied)
double_it(val);
std::cout << "After double_it: " << val << '\n'; // Now 10 (modified)
std::vector<std::string> people = {"Alice", "Bob", "Charlie"};
print_all(people); // No copy of the vector — efficient
maybe_increment(&val); // Pass address explicitly
std::cout << "After maybe_increment: " << val << '\n'; // 11
maybe_increment(nullptr); // Safe: the function checks for null
}Klicke auf Ausführen, um dieses Beispiel zu kompilieren und auszuführen.
Folge diesen Leitlinien aus den C++ Core Guidelines, wenn du entscheidest, wie du Parameter übergibst:
- Billig zu kopieren und wird nie verändert? Per Wert übergeben (int, double, char, bool, kleine Structs, std::string_view).
- Teuer zu kopieren und wird nur gelesen? Per const& übergeben (std::string, std::vector, große Objekte).
- Du musst die Variable des Aufrufers verändern? Per & übergeben (nicht-konstante Referenz).
- Könnte null sein? Per Zeiger übergeben. Erwäge aber stattdessen std::optional.
- Du übernimmst den Besitz (Sink-Parameter)? Per Wert übergeben und verschieben (behandelt im Modul zur Move-Semantik).
Als Faustregel gilt: Objekte, die größer als 2–3 Zeiger sind (16–24 Byte auf 64-Bit-Systemen), solltest du per Referenz statt per Wert übergeben.
- Übergib kleine, billig zu kopierende Typen per Wert: int, double, char, bool, string_view
- Übergib große, nur lesend genutzte Objekte per konstanter Referenz: const std::string&, const std::vector
& - Übergib per nicht-konstanter Referenz nur dann, wenn die Funktion das Objekt des Aufrufers verändern muss
- Übergib per Zeiger, wenn nullptr ein gültiges Argument ist — bevorzuge aber nach Möglichkeit std::optional
- Übergib im Zweifel per konstanter Referenz — das ist sicher und effizient
Rückgabewertoptimierung (RVO/NRVO)
Einsteiger sorgen sich oft, dass die Rückgabe großer Objekte per Wert wegen des Kopierens teuer sei. In modernem C++ ist diese Sorge dank der Return Value Optimization (RVO) und der Named Return Value Optimization (NRVO) fast immer unbegründet.
RVO (seit C++17 garantiert): Gibt eine Funktion ein temporäres Objekt zurück (z. B. return std::vector), muss der Compiler das Objekt direkt im Speicher des Aufrufers konstruieren. Es findet weder eine Kopie noch ein Move statt.
NRVO (erlaubt, aber nicht garantiert): Gibt eine Funktion eine benannte lokale Variable zurück (z. B. return result;), darf der Compiler diese Variable direkt im Speicher des Aufrufers konstruieren. In der Praxis tun das alle großen Compiler, sogar im Debug-Modus.
Der praktische Rat lautet: Gib bedenkenlos per Wert zurück. Verwende keine Ausgabeparameter (void f(std::vector), um Kopien zu vermeiden — überlass die Optimierung der Rückgabe dem Compiler.
Funktionsüberladung & Standardargumente
C++ erlaubt mehrere Funktionen mit demselben Namen, aber unterschiedlichen Parameterlisten. Der Compiler wählt anhand der an der Aufrufstelle übergebenen Argumente die beste Übereinstimmung aus:
#include <iostream>
#include <string>
#include <vector>
// Function overloading — same name, different parameter types/counts
void log(int value) {
std::cout << "[INT] " << value << '\n';
}
void log(double value) {
std::cout << "[DOUBLE] " << value << '\n';
}
void log(const std::string& value) {
std::cout << "[STRING] " << value << '\n';
}
// Default arguments — provide fallback values
void greet(const std::string& name, const std::string& greeting = "Hello") {
std::cout << greeting << ", " << name << "!\n";
}
// [[nodiscard]] (C++17) — warn if return value is ignored
[[nodiscard]] int compute(int a, int b) {
return a * b + a;
}
// [[nodiscard]] with reason (C++20)
[[nodiscard("Ignoring error codes can hide failures")]]
int open_file(const std::string& path) {
// Returns 0 on success, negative on error
return 0; // Simplified
}
// inline — suggests the compiler inline this function (also allows
// the function to be defined in a header without ODR violations)
inline int fast_max(int a, int b) {
return (a > b) ? a : b;
}
int main() {
log(42); // Calls log(int)
log(3.14); // Calls log(double)
log(std::string{"hi"});// Calls log(const string&)
greet("Alice"); // Uses default: "Hello, Alice!"
greet("Bob", "Good morning"); // Uses provided: "Good morning, Bob!"
// int result = compute(3, 4); // OK
// compute(3, 4); // WARNING: ignoring [[nodiscard]] return value
int status = open_file("data.txt"); // OK: return value used
}Klicke auf Ausführen, um dieses Beispiel zu kompilieren und auszuführen.
Die Überladungsauflösung folgt komplexen Regeln, und mehrdeutige Aufrufe sind eine häufige Ursache für Übersetzungsfehler. Achte auf diese Fallstricke:
1. Implizite Konvertierungen erzeugen Mehrdeutigkeit: Hast du f(int) und f(double), ist der Aufruf f(3.14f) (ein float) mehrdeutig, weil sich float gleich gut nach int wie nach double konvertieren lässt. Die Lösung: eine Überladung f(float) ergänzen oder static_cast verwenden.
2. Standardargumente vertragen sich schlecht mit Überladungen: void f(int x, int y = 0) und void f(int x) sind beim Aufruf mit einem Argument mehrdeutig.
3. Konstante Referenz gegenüber Wert: f(int) und f(const int&) sind bei lvalue-Argumenten mehrdeutig, weil beide gleich gut passen.
Wenn der Compiler eine mehrdeutige Überladung meldet, rate nicht herum — lies die Fehlermeldung genau. Sie nennt dir die Kandidaten und erklärt, warum sie gleich gut passen.
- Implizite Konvertierungen können die Überladungsauflösung mehrdeutig machen — ergänze explizite Überladungen, um das aufzulösen
- Standardargumente und Überladungen können kollidieren — verwende nicht beides beim selben Funktionsnamen
- Im Zweifel listet die Fehlermeldung des Compilers die infrage kommenden Überladungen und die Mehrdeutigkeit auf
- Verwende static_cast, um beim Aufruf gezielt eine Überladung auszuwählen, wenn die Argumente auf mehrere passen
- Übergib kleine Typen per Wert, große nur lesend genutzte Typen per konstanter Referenz und veränderbare Typen per nicht-konstanter Referenz
- Gib bedenkenlos per Wert zurück — RVO/NRVO eliminieren die Kopie in der Praxis
- Die Funktionsüberladung wählt anhand der Argumenttypen die beste Übereinstimmung — hüte dich vor Mehrdeutigkeit durch implizite Konvertierungen
- Verwende [[nodiscard]] bei Funktionen, deren Rückgabewert nicht ignoriert werden darf (Fehlercodes, berechnete Ergebnisse)
- Standardargumente sind bequem, können aber mit Überladungen kollidieren — nimm das eine oder das andere
- Mit inline lassen sich Funktionen in Headern definieren, ohne die One-Definition-Rule zu verletzen
Quiz — Teste dein Wissen
(15 XP)1. Wann solltest du einen `std::vector<int>` per `const&` statt per Wert übergeben?
2. Was ist die Return Value Optimization (RVO)?
3. Was bewirkt das Attribut `[[nodiscard]]`?