1. Einleitung
Wenn man anfängt, Funktionen zu schreiben, ist es sehr verlockend zu sagen: „die Funktion nimmt einfach std::string — praktisch!“. Und ja, praktisch… bis man versehentlich überall Strings kopiert, wo man es nicht geplant hat. Strings können, wie Studenten in der frühen Vorlesung, ziemlich „schwer“ sein: lang und nicht begeistert von unnötigem Verschieben.
Das heutige Ziel ist sehr pragmatisch: lernen, Funktionen so zu schreiben, dass sie „Text von überall lesen können“ (aus std::string, aus einem Stringliterale wie "hello", aus einem Teil eines Strings) und dabei keine Zeichen unnötig kopieren. Dafür haben wir den Helden des Tages — std::string_view.
Eine kurze Anmerkung „aus der Welt der Standards“: in Arbeitsdokumenten und Diskussionen des C++-Komitees tauchen regelmäßig Fragen zur Konsistenz der Schnittstellen von std::string und std::string_view auf. Das Thema ist tatsächlich praxisnah und „lebendig“, nicht nur eine Aufgabenstellung der Lehrenden.
Problem versteckter Kopien bei Übergabe von std::string per Wert
Schauen wir uns die naive Variante an:
#include <string>
std::size_t count_letters(std::string s) { // <- Kopie des Strings!
return s.size();
}
Die Funktion macht genau eine Sache: sie gibt die Länge zurück. Aber sie bekommt den String per Wert, das heißt im Sinne: „gib mir meine eigene Kopie des Texts“. Für std::string bedeutet das oft Speicherallokation und Kopieren der Zeichen (der Compiler hilft manchmal mit Optimierungen, aber sich auf „vielleicht“ zu verlassen ist eine schlechte Strategie in Programmierung und im Leben).
Man könnte natürlich sagen: „Okay, dann benutzen wir const std::string&“. Das ist deutlich besser — es entsteht keine Kopie. Aber dieser Ansatz hat eine Einschränkung: er akzeptiert nur std::string. Was ist mit Stringliteralen? Was ist mit einem Teilstring, der beim Parsen abgetrennt wurde? Dann fängt unnötiger Aufwand an.
Hier kommt std::string_view ins Spiel: man sagt der Funktion „hier ist eine Ansicht auf den Text, lies ihn, aber übernimm keine Verantwortung“.
Welchen Parametertyp wählen: std::string, const std::string& oder std::string_view
Wenn Sie den Parametertyp wählen, unterschreiben Sie faktisch einen Vertrag: wird die Funktion die Daten besitzen, wird sie kopieren, welche Textquellen werden akzeptiert. Hilfreicher als „modern/nicht modern“ ist es, an den Vertrag und die Konsequenzen zu denken.
Vergleichen wir die drei gängigen Varianten in einer Tabelle:
| Parameter | Kopiert? | Akzeptiert |
Akzeptiert |
Gut für |
|---|---|---|---|---|
|
oft ja | ja (erstellt temporären String) | ja | wenn eine Kopie/Storage/Veränderung benötigt wird |
|
nein | ja (erstellt temporären String) | ja | wenn gelesen wird und man Kopien vermeiden will, aber bereit ist, Einschränkungen zu akzeptieren |
|
nein | ja (ohne ) |
ja | wenn man lokal liest und Flexibilität möchte |
Der entscheidende Punkt: const std::string& vermeidet zwar Kopien, wenn bereits ein std::string vorliegt, aber ein Stringliteral wie "hi" ist kein std::string. Wenn f("hi") aufgerufen wird und f const std::string& erwartet, muss der Compiler einen temporären std::string erstellen. Ein std::string_view kann normalerweise direkt auf das Literal zeigen — ohne Zwischen-String.
Und genau das erleichtert vieles: eine Funktion schreiben, die mit verschiedenen Textquellen benutzt werden kann.
Warum std::string_view normalerweise per Wert übergeben wird
Bei std::string_view ist die alte Gewohnheit leicht falsch: „das ist eine String-Referenz, also besser const&“. Aber string_view ist keine datenhaltende Zeichenkette. Es ist ein kleines Beschreibungsobjekt: im Wesentlichen „Adresse + Länge“. Daher ist die Übergabe per Wert normal und sogar zu bevorzugen: man kopiert zwei Zahlen (ungefähr), nicht den gesamten Text.
Beispiel:
#include <string_view>
bool is_empty(std::string_view s) {
return s.empty();
}
Wir übergeben s per Wert, ohne die Zeichen zu kopieren: s zeigt nur auf fremden Speicher.
Um besser zu veranschaulichen, was passiert, kann man sich ein Schema vorstellen:
flowchart LR
A["std::string Besitzer
Puffer: 'h e l l o'"] --> B["std::string_view
data()+size()"]
B --> C["Funktion liest
Zeichen ohne Kopie"]
Wichtig ist: da string_view den Speicher nicht besitzt, muss er nicht „am Leben gehalten werden“. Er zeigt lediglich darauf. Wir benutzen ihn hier als Parameter — also als kurzlebige Ansicht, die während des Funktionsaufrufs gültig ist. Das ist das sicherste und häufigste Szenario.
2. Grundlegende Operationen von std::string_view zum Lesen und Parsen
Man will am liebsten sofort „alles parsen“, aber sinnvoller ist es, zunächst einen kleinen Satz an Operationen zu beherrschen, der 80% der Aufgaben abdeckt. std::string_view ist bewusst dem std::string im Leseverhalten ähnlich: es gibt size(), empty(), operator[], find(), substr(). Man sollte nicht das Gefühl haben, „ich lerne wieder eine neue Stringklasse“ — eher: „String im ReadOnly-Modus“.
Mini-Set, ohne das es schnell wehtut
Beginnen wir mit einem kurzen Beispiel: prüfen des ersten Zeichens (und nicht die empty()-Prüfung vergessen, sonst endet man im klassischen Fall „lief bis das leere String kam“).
#include <string_view>
bool starts_with_hash(std::string_view s) {
if (s.empty()) return false;
return s[0] == '#';
}
Beachte: s[0] prüft nicht die Grenzen. Das ist bewusst so: std::string verhält sich genauso. Daher ist die Prüfung mit empty() keine Dekoration, sondern handfeste Absicherung.
Nun ein nützlicheres Beispiel für CLI-Programme: Prüfung eines Befehlspräfixes.
#include <string_view>
bool has_prefix(std::string_view s, std::string_view prefix) {
if (s.size() < prefix.size()) return false;
return s.substr(0, prefix.size()) == prefix;
}
Diese Funktion kann sowohl ein std::string, als auch "cmd:" oder ein Teilstring akzeptieren — ohne den Text zu kopieren. Genau diese Flexibilität wollen wir erreichen.
find() und npos: wie man substr() nicht zur Lotterie macht
Beim Teilen eines Strings in Teile (z. B. key=value) ist die erste Idee oft: Trennzeichen finden und substr verwenden. Aber find() hat eine wichtige Eigenschaft: wenn das Zeichen nicht gefunden wird, liefert es den speziellen Wert npos. Wenn man das vergisst, wird der Code zur Frage „fällt das Programm bei dieser Eingabe aus oder nicht?\".
Schreiben wir ein sicheres split_once, das einen String am ersten Trenner aufteilt:
#include <string_view>
#include <utility>
std::pair<std::string_view, std::string_view> split_once(std::string_view s, char delim) {
std::size_t pos = s.find(delim);
if (pos == std::string_view::npos) return {s, std::string_view{}};
return {s.substr(0, pos), s.substr(pos + 1)};
}
Der zweite Teil kann leer sein — das ist normal und nützlich. Beispielsweise liefert key= einen leeren Wert auf der rechten Seite.
Kleine Überprüfung in main:
#include <iostream>
#include <string_view>
#include <utility>
int main() {
auto [left, right] = split_once("key=value", '=');
std::cout << left << " | " << right << '\n'; // key | value
}
Ein paar Worte zur Evolution des Standards: in Arbeitsdiskussionen des Komitees finden sich sogar Punkte dazu, wie string und string_view besser zusammenwirken sollten. „Strings ohne Kopien“ sind also kein seltenes Trickchen, sondern ein Entwicklungspfad zur Benutzerfreundlichkeit der Standardbibliothek.
remove_prefix() und remove_suffix(): Grenzen verschieben ohne neuen String
Eine häufige Aufgabe beim Lesen von Befehlen oder Daten ist das Entfernen von Leerzeichen an den Rändern. Mit std::string würde man substr() verwenden und neuen Speicher erzeugen oder Indizes manuell verwalten. std::string_view erlaubt eine elegantere Lösung: einfach die Ansichtsgrenzen verschieben. Zeichen bleiben unverändert, kein Speicher wird alloziert — man sagt nur: „schau nicht auf den ganzen Text, sondern auf seinen Mittelteil\".
Schreiben wir trim_spaces, das Leerzeichen an den Rändern entfernt:
#include <string_view>
std::string_view trim_spaces(std::string_view s) {
while (!s.empty() && s.front() == ' ') s.remove_prefix(1);
while (!s.empty() && s.back() == ' ') s.remove_suffix(1);
return s;
}
Test:
#include <iostream>
#include <string_view>
int main() {
std::cout << '[' << trim_spaces(" hi ") << "]\n"; // [hi]
}
Wichtiges Gefühl: trim_spaces erstellt nichts. Sie liefert eine neue string_view, die in den Ursprungsstring hineinzeigt, jedoch mit geänderten Grenzen.
Genau deshalb ist string_view ideal als Parameter: man nimmt den Input „als Ansicht“, normalisiert schnell durch Verschiebung der Grenzen und parsed dann das bereinigte Fragment — alles ohne Kopien.
3. Beispiel: Kommando-Parsing ohne Kopien, Speicherung mit Kopie
Damit die Beispiele nicht „im Vakuum“ stehen, entwickeln wir unser kleines Konsolenprogramm weiter. Stellen wir uns vor, wir haben eine einfache „ToDo-Liste“: wir lesen eine Kommandozeile wie "add Milch kaufen" und legen die Aufgabe in ein std::vector<std::string>. Ohne komplexe Modelle oder Status — nur Text, damit wir nicht vorgreifen.
Das Ziel ist klar: den Kommando-String ohne unnötige Kopien parsen, aber beim Hinzufügen einer Aufgabe eine Kopie in std::string anlegen, weil man string_view nicht dauerhaft speichern sollte (er besitzt den Speicher nicht).
parse_command: Kommando und Argument als string_view
Machen wir eine Funktion, die aus einer Zeile wie "add Milch kaufen" Kommando und Argument extrahiert:
#include <string_view>
#include <utility>
std::pair<std::string_view, std::string_view> parse_command(std::string_view line) {
line = trim_spaces(line);
auto [cmd, rest] = split_once(line, ' ');
return {cmd, trim_spaces(rest)};
}
Beachte: sowohl trim_spaces als auch split_once arbeiten mit string_view, also ohne neue Strings zu erzeugen.
Jetzt ein Ausschnitt aus main: Zeile einlesen, parsen, reagieren.
#include <iostream>
#include <string>
#include <string_view>
#include <vector>
int main() {
std::vector<std::string> tasks;
std::string line;
std::getline(std::cin, line);
auto [cmd, arg] = parse_command(line);
if (cmd == "add" && !arg.empty()) tasks.push_back(std::string(arg));
std::cout << tasks.size() << '\n'; // 1 (wenn "add something" eingegeben wurde)
}
Darin steckt der praktische Gedanke: std::string_view ist ideal zum „lesen und parsen“, aber wenn das Ergebnis unabhängig vom Ursprungsstring gespeichert werden soll, wandelt man es in ein std::string um. Im Code sieht man das deutlich: tasks.push_back(std::string(arg)).
Wenn man das zu einem kleinen Loop ausbaut, bekommt man schon etwas, das einer echten CLI-Utility gleicht (ich teile es in kleine Stücke, damit der Code übersichtlich bleibt):
#include <iostream>
#include <string>
bool read_line(std::string& line) {
std::cout << "> ";
return static_cast<bool>(std::getline(std::cin, line));
}
Und die Behandlung einiger Befehle:
#include <iostream>
#include <string>
#include <vector>
void handle(std::vector<std::string>& tasks, std::string_view cmd, std::string_view arg) {
if (cmd == "add" && !arg.empty()) tasks.push_back(std::string(arg));
if (cmd == "list") for (const auto& t : tasks) std::cout << "- " << t << '\n';
}
Beachte, wie gut die Verträge zusammenpassen: handle bekommt cmd und arg als string_view (schnelles Lesen), während tasks std::string speichert (Besitz). Das ist "richtige architektonische Faulheit": wir sparen Kopien, wo sie unnötig sind, und machen eine Kopie, wo es notwendig ist.
Kleiner humorvoller Vergleich: string_view ist wie "in den Kühlschrank gucken", und std::string ist wie "Lebensmittel kaufen und bei sich zuhause lagern". Den Zettel "bei Nachbar auf dem zweiten Regal liegt Wurst" als einzige Ernährungsstrategie zu behalten ist natürlich nicht ratsam.
4. Typische Fehler bei der Verwendung von std::string_view als Parameter
Fehler Nr.1: std::string_view akzeptieren und ihn irgendwo dauerhaft abspeichern.
Die tückischste Falle ist, dass string_view wie ein String aussieht, und man versucht, ihn in ein std::vector zu legen oder als Feld im Objekt zu speichern. Aber string_view besitzt den Speicher nicht: wenn der Ursprungsstring ändert oder verschwindet, zeigt Ihr gespeichertes View ins Nirgendwo. Sichere Gewohnheit: string_view als Parameter und lokale Variable verwenden; für dauerhafte Speicherung in ein std::string kopieren.
Fehler Nr.2: vergessen, npos nach find() zu prüfen und sofort substr() aufzurufen.
Code wie auto pos = s.find('='); return s.substr(pos + 1); sieht kompakt aus, bricht aber zusammen, wenn kein '=' in der Eingabe ist. Der richtige Ritual ist etwas länger, dafür macht man nicht aus der Benutzereingabe Russisches Roulette: zuerst pos mit std::string_view::npos vergleichen, dann substr().
Fehler Nr.3: auf s[0] oder s[s.size()-1] ohne vorherige empty()-Prüfung zuzugreifen.
Selbst wenn „logisch“ niemals ein leerer String kommen sollte, wird er kommen — der Nutzer drückt Enter, eine Datei endet, oder ein Testfall macht einen Streich. Der schützende Stil ist einfach: vor front()/back()/operator[] immer if (s.empty()) …
Fehler Nr.4: versuchen, den String über std::string_view zu ändern.
string_view ist kein Editor, sondern ein „Fenster“. Methoden wie remove_prefix/remove_suffix ändern nur die Ansichtgrenzen, nicht die Zeichen. Wenn Sie echten Text modifizieren müssen (Ersetzungen, Einfügen, Akkumulieren), dann brauchen Sie einen Besitzer — meist std::string.
Fehler Nr.5: denken, const std::string& sei immer genauso effizient wie string_view.
Für die Übergabe eines vorhandenen std::string sind beide Varianten in Ordnung. Wenn Sie allerdings Literale und Teilstrings ohne temporäre Objekte akzeptieren möchten, ist std::string_view oft bequemer: Sie erweitern die Menge der möglichen Eingaben der Funktion und provozieren weniger heimliche temporäre std::string an den Aufrufstellen.
GO TO FULL VERSION