Texto como Coleção — std::string Revisitada e o std::string_view

Texto como Coleção — std::string Revisitada e o std::string_view

Enxergar std::string como container muda o que se espera dele: tamanho, capacidade, memória contígua e as mesmas consequências de realocação do vector. Daí se chega ao string_view, que permite ler texto sem copiá-lo, desde que se aceite a contrapartida de ele não prolongar a vida de nada.
Linguagem C++

11 min de leitura

No artigo O Array que Cresce Sozinho — std::vector a Fundo conhecemos o std::vector e seu modelo de memória contígua com tamanho e capacidade. Hoje reencontramos um velho conhecido da Fase 1, o std::string, mas com um olhar novo: o de que ele é, essencialmente, um container — um std::vector<char> com talentos especiais para texto. Ver os dois lado a lado consolida um modelo mental que sustenta boa parte da STL. E, como bônus da aula, apresento um recurso do C++17 que resolve um problema real de desempenho ao lidar com texto: o std::string_view, uma "janela" para caracteres que não os copia. Juntos, string e string_view cobrem quase tudo que você fará com texto em C++ moderno.

string é um container contíguo

Tudo que você aprendeu sobre vector no artigo anterior vale para string: memória contígua, size() em tempo constante, modelo de tamanho versus capacidade, crescimento automático com realocação, e as mesmas duas formas de indexar. Na prática, std::string compartilha a maior parte da interface de std::vector:

#include <iostream>
#include <string>

int main() {
    std::string s = "abc";

    s.push_back('d');           // adiciona um caractere ao fim, como no vector
    s += "ef";                  // concatena — atalho para vários push_back

    std::cout << "conteúdo: " << s << '\n';       // abcdef
    std::cout << "tamanho: " << s.size() << '\n'; // 6
    std::cout << "primeiro: " << s.front()        // 'a'
              << " último: " << s.back() << '\n'; // 'f'

    // Percorrer caractere a caractere, como um container de char:
    for (char c : s)
        std::cout << c << '.';   // a.b.c.d.e.f.
    std::cout << '\n';

    // Indexação e checagem, exatamente como no vector:
    std::cout << s[0] << ' ' << s.at(5) << '\n';  // a f
    return 0;
}

push_back, front, back, size, [], .at(), o percurso com range-based for — é a mesma gramática do vector. Isso não é coincidência: a STL foi projetada para que containers compartilhem interfaces, de modo que o que você aprende num se transfere aos outros. std::string só adiciona, por cima disso, os talentos textuais que vimos na Fase 1 (find, substr, comparação com ==, c_str).

O problema: cópias de texto que você nem percebe

Agora o motivo do string_view. Considere uma função que apenas inspeciona texto — digamos, verifica se começa com certo prefixo. A assinatura idiomática que você aprendeu é const std::string&. Ela evita cópia quando você passa um std::string. Mas há um caso escondido:

#include <string>
#include <iostream>

bool comeca_com(const std::string& texto, const std::string& prefixo) {
    return texto.compare(0, prefixo.size(), prefixo) == 0;
}

int main() {
    std::string nome = "relatorio_2024.pdf";
    comeca_com(nome, "relatorio");   // "relatorio" é const char* → vira um std::string TEMPORÁRIO!
    return 0;
}

Quando você passa o literal "relatorio" para um parâmetro const std::string&, o C++ constrói um std::string temporário a partir dele — o que aloca memória e copia os caracteres, só para a função lê-los e jogá-los fora. Para uma função chamada milhões de vezes, esse desperdício soma. O std::string_view (C++17) foi criado exatamente para eliminá-lo.

string_view: uma janela para texto, sem posse

Um std::string_view é um objeto minúsculo que guarda apenas um ponteiro para caracteres e um tamanho. Ele não possui o texto, não aloca, não copia — é uma vista (view) para caracteres que existem em outro lugar. Trocar const std::string& por std::string_view no parâmetro faz a função aceitar std::string, literais e arrays de char sem construir nada:

#include <string_view>
#include <string>
#include <iostream>

// Aceita qualquer fonte de texto SEM copiar: std::string, literais, char[].
bool comeca_com(std::string_view texto, std::string_view prefixo) {
    return texto.substr(0, prefixo.size()) == prefixo;
}

int main() {
    std::string nome = "relatorio_2024.pdf";
    std::cout << comeca_com(nome, "relatorio") << '\n';  // 1 — sem temporário!
    std::cout << comeca_com("outro.txt", "rel") << '\n'; // 0 — literal, sem cópia
    return 0;
}

Repare: string_view tem substr, size, == e boa parte da interface de leitura de string — mas nenhum método que modifique o texto, porque ele não é dono dele. Passar texto por std::string_view (por valor, pois é minúsculo) é a forma moderna e recomendada de escrever funções que apenas leem strings. É uma atualização do padrão const std::string& que você já domina.

A armadilha honesta do string_view: ele não prolonga vidas

Prometo sempre o perigo, e o do string_view é sério porque é sutil. Como ele apenas aponta para caracteres que vivem em outro lugar, se esse "outro lugar" morrer, o string_view vira uma janela para memória inválida — o mesmo dangling das referências (artigo Referências e Ponteiros, Frente a Frente) e dos elementos de vector realocados (O Array que Cresce Sozinho). O erro clássico é criar uma view para uma string temporária:

#include <string_view>
#include <string>

std::string_view perigo() {
    std::string local = "texto";
    return local;   // PERIGO: a view aponta para 'local', que morre ao retornar.
                    // O chamador recebe uma janela para memória liberada.
}

// Outro caso comum:
// std::string_view v = std::string("temp") + "oral";
// O std::string temporário morre no fim da linha; v fica pendente.

A regra de bolso: use string_view para parâmetros de funções que leem texto (o texto original vive no chamador durante a chamada — seguro) e evite armazená-lo em membros ou retorná-lo apontando para algo temporário. Se você precisa guardar o texto, guarde um std::string (que possui), não um string_view (que só observa). É a distinção posse-versus-observação da Fase 2 reaparecendo: string é como um dono (unique_ptr), string_view é como um observador (weak_ptr, sem o .lock() de segurança).

Enxergar std::string como container muda o que se espera dele: tamanho, capacidade, memória contígua e as mesmas consequências de realocação do vector. O string_view resolve o desperdício de copiar texto só para lê-lo, e cobra a contrapartida de não possuir nada — ele é uma janela, e a janela não sustenta a parede. Apontar um string_view para um temporário, ou guardá-lo além da vida do texto original, produz leitura de memória liberada sem qualquer aviso do compilador.

Fontes e leituras recomendadas

  • cppreference.com/w/cpp/string/basic_string_view: a referência completa de std::string_view, com todos os métodos e as advertências sobre tempo de vida.
  • Bjarne Stroustrup, A Tour of C++ (3ª ed.), seção sobre strings: a apresentação de string e string_view como parte coesa do tratamento de texto.
  • ISO C++ Core Guidelines, regra F.24 e SL.str.2 ("Use std::string_view ... to refer to character sequences"): as diretrizes sobre quando usar view em vez de string.
  • cppreference.com/w/cpp/string/basic_string: para revisar a interface compartilhada com vector (push_back, front, back, at).
  • Scott Meyers, Effective Modern C++: embora anterior ao string_view, seus itens sobre passagem de parâmetros iluminam por que a view resolve o problema do temporário.

Exercícios

Exercício 1

Escreva uma função int conta_caractere(std::string_view s, char alvo) que conte quantas vezes alvo aparece em s. Chame-a com um std::string e com um literal, comprovando que ambos funcionam sem cópia.

Ver resposta

✓ Resposta: A contagem via view:

#include <iostream>
#include <string>
#include <string_view>
int conta_caractere(std::string_view s, char alvo) {
    int total = 0;
    for (char c : s) if (c == alvo) ++total;
    return total;
}
int main() {
    std::string texto = "banana";
    std::cout << conta_caractere(texto, 'a') << '\n';   // 3 (std::string, sem cópia)
    std::cout << conta_caractere("arara", 'a') << '\n'; // 3 (literal, sem temporário)
    return 0;
}

Ambas as chamadas passam pela mesma string_view sem alocar nada — o std::string é observado diretamente e o literal é apontado sem virar um std::string temporário.

Exercício 2

Explique, com base na aula, por que a interface de std::string_view não oferece um push_back nem um operator+=, enquanto std::string oferece.

Ver resposta

✓ Resposta: Porque std::string_view não possui o texto — ele é só uma janela (ponteiro + tamanho) para caracteres que vivem em outro lugar. push_back e += modificam o conteúdo, o que exigiria alocar memória e alterar o texto subjacente; uma view não tem esse direito nem essa capacidade, pois não é dona do buffer. std::string, ao contrário, possui seu buffer e pode crescê-lo, então oferece as operações de modificação. A interface de cada tipo reflete o que ele pode legitimamente fazer: observar (view) versus possuir e alterar (string).

Exercício 3

O código abaixo tem um bug de tempo de vida com string_view. Aponte-o e corrija.

#include <string_view>
#include <string>
#include <iostream>
std::string_view rotulo(int n) {
    std::string s = "item " + std::to_string(n);
    return s;   // problema?
}
int main() {
    std::cout << rotulo(7) << '\n';
    return 0;
}
Ver resposta

✓ Resposta: O bug: rotulo cria uma std::string local s e retorna uma string_view que aponta para ela; ao fim da função, s é destruída, e o chamador recebe uma view para memória liberada — comportamento indefinido ao imprimir. Correção: retornar std::string (que possui e sobrevive), não a view:

#include <string>
#include <iostream>
std::string rotulo(int n) {
    return "item " + std::to_string(n);   // devolve posse do texto
}
int main() {
    std::cout << rotulo(7) << '\n';   // item 7
    return 0;
}

Exercício 4

Reescreva a função comeca_com com assinatura const std::string& (versão antiga) e depois com std::string_view (moderna). Descreva concretamente o que acontece de diferente ao chamá-las com o literal "abc".

Ver resposta

✓ Resposta: As duas versões:

// Antiga:
bool comeca_com(const std::string& t, const std::string& p) {
    return t.compare(0, p.size(), p) == 0;
}
// Moderna:
bool comeca_com(std::string_view t, std::string_view p) {
    return t.substr(0, p.size()) == p;
}

Ao chamar com o literal "abc": na versão antiga, "abc" é convertido para um std::string temporário — o que aloca memória no heap e copia os três caracteres, só para a função lê-los e o temporário ser destruído logo após. Na versão moderna, "abc" vira uma string_view que apenas aponta para o literal (que já existe na memória do programa), sem alocação nem cópia. Para chamadas frequentes, a diferença é uma alocação de heap por chamada eliminada.

Exercício 5

Uma função precisa guardar o texto recebido dentro de um objeto para uso posterior (não só lê-lo durante a chamada). Explique por que, nesse caso, o membro deve ser std::string e não std::string_view, ligando a decisão ao conceito de posse versus observação da Fase 2.

Ver resposta

✓ Resposta: Porque guardar o texto para uso posterior significa que ele precisa continuar existindo depois que a chamada termina — o objeto precisa ser dono do texto. Um std::string_view membro apenas apontaria para um texto que vive em outro lugar; se esse lugar fosse um argumento temporário ou uma variável do chamador que sai de escopo, o membro viraria uma janela pendente assim que a chamada retornasse, e qualquer uso posterior seria comportamento indefinido. Um std::string membro copia e possui o texto, garantindo que ele viva tanto quanto o objeto. É exatamente a lição de posse versus observação da Fase 2: para guardar, você precisa de posse (std::string, análogo ao unique_ptr); a observação (string_view, análogo ao weak_ptr) só é segura enquanto o dono original está vivo, o que serve a parâmetros de passagem, não a armazenamento.

Comentários

Mais em Linguagem C++

Operações Indivisíveis — std::atomic
Operações Indivisíveis — std::atomic

Quando basta um contador ou uma flag, o mutex é peso desnecessário: o atomic…

Provando que Funciona — Testes Automatizados com Catch2
Provando que Funciona — Testes Automatizados com Catch2

Rodar e olhar a saída não protege contra regressão, porque ninguém repete a…

Extensão — Páginas Dinâmicas: HTML Gerado com Templates
Extensão — Páginas Dinâmicas: HTML Gerado com Templates

Montar HTML concatenando string quebra por dois motivos ao mesmo tempo: fica…