1. std::string_view simultaneamente utile e pericoloso
Quando vedi per la prima volta std::string_view, sembra una «stringa leggera senza copie»: la passi a una funzione — e tutto è veloce, elegante, moderno. Ed è vero, fintanto che tieni a mente un dettaglio importante: string_view non possiede il testo. Somiglia a una «finestra» sui dati altrui. La finestra è comoda per guardare, ma non puoi portare i dati a casa.
Il valore pratico di string_view è di solito in due cose. Primo, possiamo accettare testo in una funzione senza copiarlo: sia std::string, sia un literal stringa, sia un frammento di stringa. Secondo, si possono fare sottostringhe senza copia: «indica l'intervallo di caratteri».
Ma dagli stessi vantaggi nasce il rischio: se il proprietario dei dati scompare o sposta i suoi dati, il view continua a memorizzare il vecchio indirizzo e la vecchia lunghezza — e si legge non più «testo», ma un valore casuale.
Per fissare i ruoli, è utile tenere a mente questa tabella:
| Entità | Possiede memoria? | Memorizza "dove è il testo"? | Durata dei dati |
|---|---|---|---|
|
sì | sì | finché l'oggetto vive |
| literal stringa "hi" | «sì» (staticamente) | sì | fino alla terminazione del programma |
|
no | sì (indirizzo+lunghezza) | non gestisce il lifetime |
Cosa c'è realmente dentro std::string_view
È facile sbagliarsi pensando che string_view sia «quasi una string». In realtà ha una natura diversa: è solo una coppia «puntatore ai caratteri + numero di caratteri». Nessuna garanzia di null-terminator, nessuna garanzia che i dati vivano, nessuna garanzia che i dati non vengano modificati. Perciò string_view si comporta spesso come una slice di un array di caratteri.
Ecco un piccolo esempio che conviene vedere una volta:
#include <iostream>
#include <string_view>
int main() {
std::string_view sv = "hello";
std::cout << sv.size() << '\n'; // 5
std::cout << sv << '\n'; // hello
}
Perché questo funziona in modo sicuro? Perché "hello" è un literal stringa. Vive molto a lungo (praticamente "sempre"), quindi il view su di esso non diventa dangling.
Idea utile: std::string_view può puntare a dati che non sono affatto null-terminated. Per esempio, al centro di una stringa. Questo è normale. Ma significa che qualsiasi codice che «passa data() dove si aspetta una C-string» può rompersi inaspettatamente. E di solito si rompe proprio il giorno in cui sei sicuro che «tutto è testato».
2. Legge del giorno: il view non deve sopravvivere al proprietario
Il concetto centrale dell'intera lezione suona quasi come una regola di sicurezza: il view non deve vivere più a lungo del proprietario dei dati. Se ricordi solo una frase — sia questa.
Analogia comoda: std::string è un libro, std::string_view è un segnalibro con numero di pagina e riga. Il segnalibro non contiene il testo. Contiene "dove leggere". Se il libro viene buttato via, il segnalibro resta, ma non c'è più nulla da leggere. E se il libro viene rilegato diversamente e le pagine si spostano, il segnalibro punta al nulla pur sembrando perfetto.
Un altro dettaglio importante: il lifetime del view (variabile std::string_view) e il lifetime dei dati su cui punta sono cose diverse. Il view può vivere a lungo mentre i dati sono già morti. Ed è proprio questo che crea la situazione di "dangling string_view".
3. Trappole comuni sul lifetime di std::string_view
Trappola №1: string_view da una std::string temporanea
L'errore più comune e più frustrante: creare una stringa temporanea e subito farne un string_view. Il codice compila perfettamente. In review sembra "moderno". Poi iniziano i crash misteriosi "solo in produzione e solo il venerdì".
#include <iostream>
#include <string>
#include <string_view>
int main() {
std::string_view sv = std::string("hi");
std::cout << sv << '\n'; // UB: sv guarda una stringa già distrutta
}
Perché succede? Perché std::string("hi") è un oggetto temporaneo. Vive fino alla fine dell'espressione completa, cioè più o meno fino al ;. Dopo il ; la stringa temporanea viene distrutta, mentre sv resta con un indirizzo al "passato".
Stringhe temporanee nascoste: substr di std::string
C'è una variante ancora più subdola. Potresti non creare manualmente std::string("..."). Basta chiamare un metodo che ritorna una std::string, non un string_view. Per esempio, std::string::substr.
#include <string>
#include <string_view>
int main() {
std::string s = "command: add milk";
std::string_view v = s.substr(0, 7); // UB: substr ha restituito una std::string temporanea
(void)v;
}
Qui il problema non è s — lui vive. Il problema è che s.substr(...) ha creato una nuova stringa (temporanea), e il view è stato costruito su di essa.
Come fare correttamente? Se vuoi davvero un view senza copie, costruiscilo dalla stringa originale e poi fai substr sul view:
#include <string>
#include <string_view>
int main() {
std::string s = "command: add milk";
std::string_view v = std::string_view{s}.substr(0, 7); // OK
(void)v;
}
Ora substr fa la slice dentro il view, e il view punta alla memoria di s, che è viva.
Sfondo pratico: perché qui è facile sbagliare
Anche nel committee C++ si sono discusse le constructor details di string_view per gli scenari range e la rigidità delle conversioni — cioè il tema "come è facile costruire accidentalmente un view dalla fonte sbagliata" è reale e doloroso. Per esempio si è discusso se il range-constructor di string_view dovesse essere explicit.
Trappola №2: la funzione ritorna string_view, ma al suo interno crea una stringa
Dopo la prima trappola è naturale cadere nella seconda: "ok, non creo temporanei in main, scrivo una funzione che ritorna un view". E qui quasi certamente appare il dangling, perché la stringa dentro la funzione è locale e quindi muore all'uscita dalla funzione.
#include <string>
#include <string_view>
std::string_view bad_prefix() {
std::string p = "cmd:";
return p; // UB: p sarà distrutta all'uscita dalla funzione
}
È l'analogo diretto di "ritornare una reference a una variabile locale", ma nel mondo delle stringhe.
Ritornare string_view è possibile, ma solo se il view punta a dati che sicuramente vivono più a lungo. Per esempio, a un literal stringa:
#include <string_view>
std::string_view ok_prefix() {
return "cmd:"; // OK: il literal vive fino alla fine del programma
}
Oppure su una stringa passata dall'esterno (ma qui sorge il rischio successivo: il chiamante potrebbe aver passato una stringa temporanea).
Trappola №3: ritorni un view sul parametro, ma ti passano un oggetto temporaneo
Questa trappola appare particolarmente elegante. Scrivi una funzione "senza copie", accetti std::string_view, ritorni std::string_view — perfetto. Il problema è che il contratto dipende da cosa viene passato.
#include <string_view>
std::string_view first_word(std::string_view line) {
std::size_t pos = line.find(' ');
return (pos == std::string_view::npos) ? line : line.substr(0, pos);
}
Ora una chiamata pericolosa:
#include <iostream>
#include <string>
#include <string_view>
int main() {
std::string_view w = first_word(std::string("add milk"));
std::cout << w << '\n'; // UB: la stringa temporanea è morta dopo ';'
}
La funzione first_word è "onesta": opera sul view e ritorna un view sullo stesso buffer. Ma il contratto della funzione è sottile: "il valore ritornato è valido finché la sorgente del testo vive".
Come rendere il comportamento più robusto per codice di livello iniziale? Spesso la risposta è semplice: ritornare una std::string per valore se il risultato si intende conservare.
#include <string>
#include <string_view>
std::string first_word_copy(std::string_view line) {
std::size_t pos = line.find(' ');
std::string_view w = (pos == std::string_view::npos) ? line : line.substr(0, pos);
return std::string(w); // copio esattamente il pezzo necessario
}
Sì, è una copia. Ma il contratto diventa trasparente: il risultato vive da solo.
Trappola №4: memorizzare std::string_view come campo di una struct
Quando inizi a modellare dati, può sorgere la tentazione: "memorizziamo string_view invece di string per essere più veloci". Questo è uno dei micro‑guadagni più costosi nella tua vita, perché spesso si trasforma in un campo minato di lifetime.
Un modello molto pessimo assomiglia a questo:
#include <string_view>
struct Task {
int id = 0;
std::string_view title; // pericoloso: chi è il proprietario del testo?
bool done = false;
};
Perché è pericoloso? Perché Task ora memorizza un "segnalibro". Dov'è il libro? Chi garantisce che il libro non scompaia? Di solito — nessuno.
La versione "noiosa" corretta — possedere la stringa:
#include <string>
struct Task {
int id = 0;
std::string title; // possessiamo
bool done = false;
};
Usa string_view dove è davvero utile: parametri di input, parsing, confronti, ricerche, ma non come storage a lungo termine.
Trappola №5: view su una std::string, poi la stringa viene modificata
Anche se il proprietario dei dati vive abbastanza a lungo, rimane un'altra ragione per cui il view può "rompersi": il proprietario può modificare il buffer. Per std::string questo significa che modificandola la stringa potrebbe riallocare la memoria, spostarsi altrove — e il vecchio indirizzo non punta più ai dati correnti.
#include <iostream>
#include <string>
#include <string_view>
int main() {
std::string s = "abc";
std::string_view v = s;
s += "defghijklmnopqrstuvwxyz"; // può riallocare il buffer
std::cout << v << '\n'; // UB, se il buffer si è spostato
}
L'insidia è che a volte il buffer non si sposta (ad esempio per small string optimization o perché la capacity() è già sufficiente). Quindi l'errore può non manifestarsi su stringhe piccole e manifestarsi su stringhe grandi, su un altro compilatore o semplicemente in una diversa fase della luna.
Conclusione pratica per codice iniziale: se hai preso un string_view su una stringa, non modificare quella stringa finché il view è necessario. E se devi modificare la stringa — ricrea il view dopo la modifica, trattandolo come "una reference usa e getta alla versione corrente".
4. Stile sicuro per TaskBoard: accettiamo view, memorizziamo string
Non vogliamo solo spaventare, ma costruire uno stile operativo realmente applicabile nel progetto. Un pattern di base buono è questo: nei modelli e nello storage possediamo i dati, e usiamo string_view al confine delle funzioni — come parametro "leggi subito".
Supponiamo di avere l'aggiunta di un task. La signature comoda è: accettiamo std::string_view title, ma nel modello inseriamo una copia (possesso).
#include <string>
#include <string_view>
#include <vector>
struct Task {
int id = 0;
std::string title;
bool done = false;
};
void add_task(std::vector<Task>& tasks, int id, std::string_view title) {
tasks.push_back(Task{id, std::string(title), false});
}
Nota l'idea: il view vive solo durante la chiamata a add_task, e dentro Task trasformiamo subito in una stringa possessiva. Questo è il momento in cui la copia è giustificata: convertiamo il testo "prestato" esterno nel nostro stato "proprio".
Uno stile analogo è utile anche per la ricerca. Per esempio, verificare se il titolo inizia con "#":
#include <string_view>
bool starts_with_hash(std::string_view s) {
return !s.empty() && s[0] == '#';
}
Questa funzione è ideale per string_view, perché non salva nulla né ritorna un borrowing esterno. Legge e risponde.
Mini set di parsing con string_view
Per vedere string_view "nel suo elemento", consideriamo un piccolo parser di comandi per TaskBoard. L'utente inserisce righe come "add Buy milk" o "done 3". Leggiamo la riga in std::string line e poi la parsifichiamo evitando copie intermedie inutili.
Cominciamo con un semplice "trim a sinistra". Ritorna string_view, ed è corretto, perché il view ritornato guarda gli stessi dati originali.
#include <string_view>
std::string_view ltrim(std::string_view s) {
while (!s.empty() && s.front() == ' ') {
s.remove_prefix(1);
}
return s;
}
Ora "split una sola volta" sul primo spazio: comando e resto della riga.
#include <string_view>
#include <utility>
std::pair<std::string_view, std::string_view> split_once(std::string_view s) {
s = ltrim(s);
std::size_t pos = s.find(' ');
if (pos == std::string_view::npos) return {s, std::string_view{}};
return {s.substr(0, pos), ltrim(s.substr(pos + 1))};
}
Queste funzioni sono sicure a condizione che il view d'ingresso sia valido. Nello stile proposto ciò è facile: leggiamo la riga in std::string line, passiamo std::string_view{line} al parser, e il parser opera solo durante l'iterazione corrente del comando.
Ora produciamo il risultato del parsing che possiede i dati. Questo è il punto chiave: il parser può usare view internamente, ma all'esterno restituisce stringhe possessive.
#include <string>
struct ParsedAdd {
std::string title;
};
E la funzione che analiza il comando add:
#include <optional>
#include <string>
#include <string_view>
std::optional<ParsedAdd> parse_add(std::string_view line) {
auto [cmd, rest] = split_once(line);
if (cmd != "add" || rest.empty()) return std::nullopt;
return ParsedAdd{std::string(rest)};
}
Qui avviene la matematica corretta del lifetime: rest è un view su line, ma lo copiamo immediatamente in std::string. Quindi ParsedAdd può essere memorizzato e usato in sicurezza ovunque.
Ed è qui che comincia la disciplina matura: se restituisci un oggetto che vivrà oltre, quell'oggetto deve possedere ciò che gli serve. Il view è utile come strumento temporaneo interno alla funzione, ma è inadatto come parte del risultato se non vuoi imporre nel contratto "la sorgente vive sicuramente".
5. Promemoria: dove string_view è appropriato e dove è pericoloso
Quando scrivi codice la mente ama "semplificare": se il tipo sembra adatto, allora va bene. Ma con string_view non funziona così — serve un rapido controllo sul lifetime. La buona notizia: è un controllo veloce se ti fai le domande giuste.
| Punto nel codice | di solito... |
Perché |
|---|---|---|
| parametro di funzione "leggi testo" | appropriato | vive solo durante la chiamata |
| variabile locale dentro la funzione | appropriato | controlli l'ordine delle operazioni |
| valore di ritorno di funzione | rischioso | il chiamante potrebbe conservarlo più a lungo della sorgente |
campo di |
quasi sempre rischioso | il campo vive a lungo mentre il proprietario dei dati potrebbe non vivere |
| view su una stringa che poi modifichi | rischioso | il buffer può spostarsi |
E ora lo stesso controllo sotto forma di piccolo diagramma a blocchi:
flowchart TD
A["Voglio usare string_view"] --> B{"Dove vivrà?"}
B -->|Solo dentro la chiamata/funzione| C["Di solito sicuro"]
B -->|Lo memorizzo in campo / globale / contenitore| D{"Esiste una garanzia forte sul lifetime del proprietario?"}
D -->|Sì, il proprietario vive più a lungo e non modifica il buffer| E["Si può fare, ma il contratto deve essere esplicito"]
D -->|No / non sono sicuro| F["Memorizza std::string (possiedi i dati)"]
Se ti sorprendi a pensare "beh, probabilmente il proprietario vivrà..." — è quasi sempre un segnale: meglio possedere la stringa. std::string costa meno di una settimana di debugging.
6. Errori tipici usando std::string_view
Errore №1: std::string_view sv = std::string("..."); e la convinzione "lo uso subito, non succede niente".
È una delle trappole più comuni perché sembra un'ottimizzazione: niente copie, tutto moderno. Nella pratica la std::string temporanea muore alla fine dell'espressione completa, mentre sv rimane. Abitudine corretta: se la sorgente è temporanea — o crea una std::string possessiva, o non conservare il view oltre l'espressione.
Errore №2: std::string_view v = s.substr(...) su std::string.
Sembra intuitivo che "substr è una sottostringa, quindi è view". Ma per std::string la substr restituisce una nuova std::string, quindi crea un temporaneo e il view finirà per guardare quello. È molto più sicuro fare il substr su std::string_view{s} — così ottieni davvero una slice senza copia.
Errore №3: ritornare std::string_view da una funzione dove la stringa è creata internamente.
È l'analogo del ritornare una reference a una variabile locale. Se la stringa è creata dentro la funzione, sarà distrutta all'uscita e il view diventerà dangling. La cura è onesta: ritornare una std::string per valore, o ritornare un view solo su dati il cui lifetime è garantito più lungo (es. literal).
Errore №4: memorizzare std::string_view nel modello dati (es. struct Task { std::string_view title; }).
Questo design fa dipendere il sistema da un proprietario esterno dei testi. Nelle applicazioni reali porta quasi sempre a situazioni "ieri funzionava". Stile più robusto: memorizzare std::string nel modello, usare string_view in parametri e parsing.
Errore №5: tenere un view su una std::string, poi modificare quella stringa.
Anche se la stringa è "viva", il suo buffer può riallocare durante +=, append(), insert() e simili. Il vecchio view non si aggiornerà e continuerà a puntare a un indirizzo obsoleto. In uno stile sicuro o non modifichi la stringa mentre il view è usato, o ricrei il view dopo la modifica, o copi il frammento necessario in una std::string.
GO TO FULL VERSION