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
stringestring_viewcomo 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.