1. Warum man const T& braucht
Wenn Sie gerade anfangen, Funktionen zu schreiben, neigt man dazu, Parameter nach dem Motto „Hauptsache es kompiliert“ zu wählen. Sehr schnell merkt man jedoch, dass das Kopieren großer Objekte (Strings, Vektoren, Strukturen mit vielen Feldern) teuer sein kann, und dass T& ein zu „starker“ Vertrag ist: er verspricht, dass die Funktion das Argument ändern kann. const T& ist ein guter Kompromiss: keine Kopie, aber nur Lesen, zusätzlich ist es kompatibel mit temporären Werten.
Es ist praktisch, so zu denken: const T& bedeutet „ich leihe mir Ihr Objekt für die Dauer des Aufrufs und verpflichte mich, nichts daran zu verändern“.
Was const in const T& bedeutet
Wichtig ist eine kurze Einleitung: das Wort const in C++ schreckt Anfänger oft ab, weil es so wirkt, als würde „alles verboten“. Tatsächlich steht const meist nicht für Verbot, sondern für einen Vertrag. Sie sagen dem Compiler (und dem Leser des Codes): „Über diesen Namen werde ich das Objekt nicht ändern“. Und wenn Sie es versehentlich versuchen — der Compiler hält Sie davon ab, wie ein Museumswächter: „Nicht mit den Händen anfassen“.
Kerngedanke:
const T& ist eine Referenz auf T, aber über diese Referenz darf das Objekt nicht verändert werden.
Das Objekt selbst kann allerdings nicht-const sein.
Schauen wir uns ein kleines Beispiel an:
#include <iostream>
int main() {
int x = 10;
const int& r = x; // r — lesendes Alias für x
std::cout << r << '\n'; // 10
// r += 1; // Kompilierfehler: darf über const-Referenz nicht verändert werden
}
Hier ist x ein ganz normaler int, er kann verändert werden. Aber über den Namen r ist das nicht möglich. Das ist der Vertrag.
Woran man eine const T& binden kann
Referenzen in C++ binden nach bestimmten Regeln (oft „binding rules“ genannt). Diese Regeln sind einer der Gründe, warum const T& in echtem Code häufiger vorkommt als einfaches T&.
Kurz gesagt: T& ist die „schreibende“ Referenz, und const T& ist die „lesende“. Daher erlauben const T& mehr Bindungsfälle, weil sie sicherer sind: man kann nicht verändern, was nicht verändert werden darf oder sollte.
Mini-Kompatibilitätstabelle
| Was rechts steht (Ausdruck) | Kann an T& gebunden werden | Kann an const T& gebunden werden |
|---|---|---|
| Normale Variable T x | Ja | Ja |
| Konstante const T cx | Nein | Ja |
| Temporärer Wert (temporary), z. B. T() | Nein | Ja |
Das ist eine intuitive Version der Regeln, ausreichend für unser Niveau. Im Standard gibt es formale Formulierungen und sogar spezielle Diskussionen zu „temporaries bound to references“ und „lifetime extension“, weil das Thema fein ist.
Beispiel: T& bindet sich nicht an const T
#include <string>
int main() {
const std::string name = "Alice";
// std::string& bad = name; // Fehler: man darf keine schreibende Referenz auf ein const-Objekt geben
const std::string& ok = name; // OK: ich verspreche, nicht zu ändern
(void)ok;
}
Die Logik ist einfach: wenn std::string& an ein const std::string gebunden würde, könnte man die „konstante“ Zeichenkette ändern — das würde die Idee von const untergraben.
2. Temporäre Objekte und Lebensdauerverlängerung
Was temporäre Objekte sind und warum const T& mit ihnen harmoniert
Temporäre Objekte sind die „One-shot“-Objekte in C++: sie entstehen als Ergebnis eines Ausdrucks und verschwinden meist sehr schnell. Anfänger bemerken sie oft nicht, weil sie keinen Namen haben. Der Compiler erzeugt sie aber, und die Regeln zur Lebensdauer sind hier wichtig.
Beispiele für temporäre Werte:
- das Ergebnis von a + b (bei Zahlen),
- das Ergebnis von std::string("Hi"),
- das Ergebnis der Konkatenation von Strings s1 + s2.
Beispiel: const int& zu einer temporären Zahl
#include <iostream>
int main() {
const int& answer = 42; // 42 — temporärer Wert
std::cout << answer << '\n'; // 42
}
Warum ist das erlaubt? Weil answer das temporäre Objekt nicht verändern kann (es ist const), also ist das sicher.
Mit einer nicht-const-Referenz int& ginge das nicht:
int main() {
// int& r = 42; // Fehler: man kann T& nicht an ein temporäres binden
}
Lebensdauerverlängerung eines Temporären
Vorsichtig formuliert: temporäre Objekte leben normalerweise nur kurz. Häufig bis zum Ende des „full expression“ (umgangssprachlich: bis zum Ende der Zeile/des Ausdrucks). Es gibt aber eine wichtige Regel: wenn ein temporäres Objekt an eine Variable vom Typ const T& gebunden wird, dann wird die Lebensdauer des temporären Objekts bis zum Ende des Sichtbarkeitsbereichs dieser Referenz verlängert.
Diese Regel ist einer der Hauptgründe, warum const T& so beliebt ist. Und ja, es gibt dazu im Standard spezielle Feinheiten und Diskussionen (z. B. über „lifetime extension of references“ in unterschiedlichen Situationen).
Beispiel: eine temporäre Zeichenkette „lebt länger, als man denkt“
#include <iostream>
#include <string>
int main() {
const std::string& s = std::string("hello"); // temporärer std::string
std::cout << s << '\n'; // hello
}
Ohne die Regel der Lebensdauerverlängerung sähe das aus wie „Referenz auf eine verschwindende Zeichenkette“, aber hier ist alles in Ordnung: das temporäre Objekt lebt so lange wie s.
Schaubild zur Lebensdauer
flowchart LR
A["Created temporary: std::string('hello')"] --> B["Bound to const std::string& s"]
B --> C["temporary lebt bis zum Ende des Scopes von s"]
C --> D["Scope endet: s wird zerstört, temporary ebenfalls"]
Falls mermaid in Ihrer Umgebung nicht gerendert wird — kein Problem. Die Idee ist einfach: die Referenz s „hält“ das temporäre Objekt lebendig, aber nur innerhalb ihres eigenen Gültigkeitsbereichs.
Temporäre und Funktionsparameter: wo die Grenze der Sicherheit liegt
Eine ehrliche Klarstellung, damit kein gefährlicher Mythos entsteht: „jede const-Referenz macht alles unsterblich“. Wenn const T& ein Funktionsparameter ist, dann lebt der temporäre Wert, der im Aufruf übergeben wurde, lange genug, damit die Funktion normal ausgeführt wird, aber das bedeutet nicht, dass Sie die Referenz nach außen tragen und danach weiterverwenden dürfen.
Praktisch merken wir uns dieses Modell: „Ein Parameter vom Typ const T& ist sicher zum Übergeben von Temporären, weil das Temporäre den Funktionsaufruf überlebt“. Danach (nach Verlassen der Funktion) gibt es jedoch keine Garantien mehr.
Hier ein sicheres Beispiel:
#include <iostream>
#include <string>
void print_line(const std::string& text) {
std::cout << text << '\n';
}
int main() {
print_line("Hi!"); // String-Literal wird zu einem temporären std::string
}
Hier ist alles gut: das temporäre String-Objekt existiert für die Dauer des Aufrufs von print_line.
Wenn man innerhalb von print_line jedoch versuchen würde, die Referenz „irgendwo global zu speichern“, landet man schnell in der Welt der dangling references. Dieses Thema besprechen wir ausführlicher in Vorlesungen über Rückgabe von Referenzen und über Lebensdauern. Heute nur die Einführung, ohne tiefe Fallen.
3. Praxis: Funktionsparameter und auto
const T& als Standard für lesende Parameter
Die häufigste reale Situation: Sie schreiben eine Funktion, die ein String-, Vektor- oder Model-Objekt lesen soll, aber nicht verändern darf. Wenn Sie den Parameter per Wert annehmen, entsteht eine Kopie. Wenn Sie T& nehmen, erlauben Sie das Ändern des Objekts (und schließen temporäre Werte aus). Daher ist die typische Wahl — const T&.
Das ist so verbreitet, dass es in C++ eine „Volksregel“ gibt: „Wenn eine Funktion ein Objekt nicht verändern soll und das Objekt schwergewichtig ist — nehme es als const&“. Für int ist das meist nicht nötig, für std::string und std::vector dagegen meistens sinnvoll.
Beispiel: String-Länge ohne Kopie
#include <iostream>
#include <string>
std::size_t len(const std::string& s) {
return s.size();
}
int main() {
std::string name = "Alice";
std::cout << len(name) << '\n'; // 5
std::cout << len("Hello") << '\n'; // 5
}
Beachten Sie: len("Hello") funktioniert gerade, weil der Parameter eine const std::string& ist. Mit einer nicht-const-Referenz std::string& würde das nicht klappen.
Anwendungsbeispiel: Ausgabe eines Task
Angenommen, im Verlauf des Kurses haben wir bereits eine kleine Konsolenanwendung „Aufgabenliste“ (nicht weil alle unbedingt eine TODO-Liste schreiben müssen, sondern weil sie sich gut als Beispiel eignet).
Unser Task-Modell:
#include <string>
struct Task {
int id = 0;
std::string title;
bool done = false;
};
Nun möchten wir eine Aufgabe auf dem Bildschirm ausgeben. Ausgeben heißt: Felder lesen, aber die Aufgabe nicht verändern. Das ist ein klarer Kandidat für const Task&.
#include <iostream>
#include <string>
struct Task {
int id = 0;
std::string title;
bool done = false;
};
void print_task(const Task& t) {
std::cout << "#" << t.id << " " << t.title;
std::cout << (t.done ? " [done]\n" : " [todo]\n");
}
int main() {
Task a{1, "Read about const references", false};
print_task(a); // #1 Read about const references [todo]
}
Warum ist das guter Stil?
- Erstens: wir kopieren Task nicht (und darin ist ein std::string, dessen Kopie spürbar teurer sein kann als ein int).
- Zweitens: aus der Signatur ist ersichtlich: die Funktion hat das Recht nicht, die Aufgabe zu verändern. Wenn Sie versehentlich etwas wie t.done = true; einfügen, stoppt Sie der Compiler.
Häufiger Lernfehler: Task per Wert annehmen
void print_task(Task t) { // Kopie!
// ...
}
So zu schreiben ist nicht verboten und in kleinen Beispielen „funktioniert“ es. Es ist aber vergleichbar damit, mit einem Lieferwagen Brot einkaufen zu fahren: möglich, aber seltsam.
Feinheit mit auto: wie man keine unnötige Kopie macht
Manchmal schreibt man schön:
auto x = something();
Und denkt, dass x eine „Referenz“ sei. Aber auto ohne & erzeugt eine Kopie, falls der rechte Ausdruck eine Referenz ist. Das ist kein Sprachfehler — Sie haben einfach nach einer Kopie gefragt.
Im Kontext von const T& ist eine gute Gewohnheit: wenn Sie „nur lesen, ohne Kopien“ wollen, dann benötigen Sie meistens const auto&.
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{10, 20, 30};
const auto& first = v[0]; // lesen ohne Kopie (obwohl int günstig ist)
std::cout << first << '\n'; // 10
}
Ja, für int ist das nicht kritisch. Für std::string und Ihre Modelle hingegen sehr wohl.
4. Typische Fehler bei der Arbeit mit const T&
Fehler Nr. 1: versuchen, das Objekt über eine const T& zu verändern und sich dann über den Compiler zu ärgern.
Der Compiler ist nicht boshaft — er erfüllt genau Ihre Vorgabe. Wenn der Parameter const Task& t ist, dann haben Sie versprochen „nicht zu ändern“. Wenn die Programmlogik dennoch Änderung verlangt, muss der Vertrag anders aussehen: Task& (ich ändere auf jeden Fall) oder Task* (ich ändere, falls der Pointer nicht nullptr), aber das ist ein separates Thema zur API-Gestaltung.
Fehler Nr. 2: glauben, dass const T& temporäre immer „für immer“ verlängert.
Die Lebensdauerverlängerung funktioniert in konkreten Szenarien, insbesondere wenn ein temporäres an eine Variable vom Typ const T& bei der Initialisierung gebunden wird. Diese Regel existiert, aber sie macht temporäre nicht unsterblich — sie haben weiterhin eine Lebensdauergrenze, nur manchmal ist diese weiter als „bis zum Ende der Zeile“.
Fehler Nr. 3: eine const T& länger speichern, als das referenzierte Objekt lebt.
const macht eine Referenz nicht automatisch „zeitlich sicher“. Sie verbietet nur das Verändern. Wenn das Objekt zerstört ist, wird die Referenz zur dangling reference — und das ist dann nicht mehr eine Frage von „dürfen/nicht dürfen ändern“, sondern von „Objekt existiert nicht mehr“. Diese Idee ist besonders wichtig, wenn Sie Referenzen auf Container-Elemente oder auf lokale Variablen aus anderen Scopes speichern.
Fehler Nr. 4: überall routinemäßig T& verwenden und sich dann wundern, dass temporäre Werte nicht akzeptiert werden.
Wenn eine Funktion das Argument nicht ändern soll — schreiben Sie besser sofort const T&. Dann können Sie normale Variablen, const-Objekte und temporäre Werte übergeben. Das macht die API freundlicher und leichter zu benutzen, besonders bei Hilfsfunktionen wie Drucken, Prüfen oder Formatieren.
Fehler Nr. 5: alles pauschal „optimieren“ durch const&, auch kleine Typen.
Für int, double, char ist die Übergabe per Wert in der Regel einfacher und genauso performant. const& ist kein magischer Knopf zur Beschleunigung, sondern ein Werkzeug für Fälle, in denen Kopien spürbar sind oder in denen Sie temporäre Werte ohne Overload akzeptieren wollen. Ja, manchmal benutzt man const& auch bei kleinen Typen der Einheitlichkeit halber — das ist dann Stil- und Teamfrage.
GO TO FULL VERSION