CodeGym /Corsi /C++ SELF /string_view‑trappole: approfondimento delle regole sul li...

string_view‑trappole: approfondimento delle regole sul lifetime

C++ SELF
Livello 47, Lezione 0
Disponibile

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
std::string
sì sì finché l'oggetto
std::string
vive
literal stringa "hi" «sì» (staticamente) sì fino alla terminazione del programma
std::string_view
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
std::string_view
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
struct/class
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.

1
Compito
C++ SELF, livello 47, lezione 0
Bloccato
Verifica del prefisso
Verifica del prefisso
1
Compito
C++ SELF, livello 47, lezione 0
Bloccato
Finestra nella stringa
Finestra nella stringa
1
Compito
C++ SELF, livello 47, lezione 0
Bloccato
La prima parola
La prima parola
1
Compito
C++ SELF, livello 47, lezione 0
Bloccato
Note e tag
Note e tag
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION