1. Introdução
Quando você começa a escrever funções, é muito natural fazer assim: “que a função aceite std::string — é conveniente!”. E sim, é conveniente… até você começar a copiar strings onde não pretendia. E strings, como estudantes numa aula matinal, às vezes são “pesadas”: longas e avessas a movimentos desnecessários.
O objetivo de hoje é muito prático: aprender a escrever funções que “sabem ler texto de qualquer lugar” (de std::string, de literal "hello", de um pedaço de string) sem copiar caracteres sem necessidade. Para isso temos o herói do dia — std::string_view.
Uma observação "do mundo dos standards": em materiais de trabalho e discussões do comitê C++ surgiram questões sobre a consistência das interfaces de std::string e std::string_view. O tema é realmente prático e atual, não inventado por instrutores só para exercício.
Problema das cópias ocultas ao passar std::string por valor
Vejamos a versão ingênua:
#include <string>
std::size_t count_letters(std::string s) { // <- cópia da string!
return s.size();
}
A função faz exatamente uma coisa: retorna o comprimento. Mas ela recebe a string por valor, ou seja, em essência pede: “me dê minha cópia pessoal do texto”. Para std::string isso muitas vezes significa alocação e cópia dos caracteres (o compilador às vezes ajuda com otimizações, mas confiar no “talvez” é uma má estratégia em programação e na vida).
Claro, podemos dizer: “Ok, então vamos usar const std::string&”. E isso já é bem melhor — não haverá cópia. Mas essa abordagem tem uma limitação: aceita apenas std::string. E se tivermos um literal? E se tivermos um pedaço de string extraído durante o parsing? Começa a dar trabalho extra.
Aqui entra std::string_view: dizemos à função “aqui está uma visão do texto, leia, mas não possua”.
Que tipo escolher: std::string, const std::string& ou std::string_view
Ao escolher o tipo de parâmetro, você na prática assina um contrato: a função terá propriedade dos dados? vai copiar? que fontes de texto aceita? É útil pensar não em “está na moda”, mas no contrato e nas consequências.
Comparemos três variantes populares em uma tabela:
| Parâmetro | Copia? | Aceita |
Aceita |
Bom para |
|---|---|---|---|---|
|
frequentemente sim | sim (criará string temporária) | sim | quando precisa de cópia/armazenamento/modificação |
|
não | sim (criará string temporária) | sim | quando lemos e queremos evitar cópia, mas aceitamos limitações |
|
não | sim (sem ) |
sim | quando lemos texto “aqui e agora” e queremos flexibilidade |
O ponto chave: const std::string& realmente evita cópias se a entrada já for std::string, mas um literal "hi" não é std::string. Portanto ao chamar f("hi"), se f aceita const std::string&, o compilador terá que criar uma std::string temporária. Já std::string_view geralmente pode olhar diretamente para o literal — sem string intermediária.
E isso dá a sensação de “uau, ficou mais simples”: você escreve uma função e pode usá-la com diferentes fontes de texto.
Por que std::string_view costuma ser passado por valor
Com std::string_view é fácil cair no hábito antigo: “se é string, devo passar por const&”. Mas string_view não é uma "string com dados". É um pequeno objeto-descritivo: basicamente "endereço + tamanho". Portanto passá-lo por valor é normalmente aceitável e até preferível: você copia dois números (ou algo próximo), não o texto inteiro.
Fica assim:
#include <string_view>
bool is_empty(std::string_view s) {
return s.empty();
}
Passamos s por valor, mas não copiamos os dados: s apenas aponta para memória alheia.
Para visualizar, podemos desenhar um esquema:
flowchart LR
A["std::string proprietário
buffer: 'h e l l o'"] --> B["std::string_view
data()+size()"]
B --> C["função lê
caracteres sem cópia"]
Importante lembrar: como string_view não possui a memória, ele também não é responsável por “mantê-la viva”. Ele apenas aponta. Hoje usamos ele como parâmetro — uma "view" de curta duração, criada para o tempo da chamada. Esse é o cenário mais seguro e frequente.
2. Operações básicas de std::string_view para leitura e parsing
É tentador já começar a "parsear tudo", mas é mais útil primeiro dominar um pequeno conjunto de operações que cobrem 80% das tarefas. std::string_view foi feito para se assemelhar a std::string em leitura: tem size(), empty(), operator[], find(), substr(). Ou seja, não parece que você precisa reaprender tudo — é mais uma "string modo somente-leitura".
Mini-conjunto sem o qual dói rápido
Comecemos com um exemplo curto: verificar o primeiro caractere (e não esquecer empty(), porque caso contrário vem a clássica "tudo funcionou até chegar uma string vazia").
#include <string_view>
bool starts_with_hash(std::string_view s) {
if (s.empty()) return false;
return s[0] == '#';
}
Repare: s[0] não checa limites. Isso é intencional: std::string age igual. Então checar empty() é proteção real, não enfeite.
Agora um exemplo mais útil para aplicações CLI: checar prefixo de comando.
#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;
}
Essa função pode aceitar std::string, "cmd:", e pedaços de string — sem copiar o texto. É essa flexibilidade que buscamos.
find() e npos: como não transformar substr() em loteria
Quando começamos a dividir string em partes (por exemplo, key=value), o primeiro impulso é localizar o separador e fazer substr. Mas find() tem uma particularidade: se o caractere não for encontrado, retorna um valor especial npos. Se você esquecer de checar isso, seu código vira um jogo de "adivinhe se o programa vai falhar com essa entrada".
Vamos escrever um split_once seguro, que divide na primeira ocorrência do separador:
#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)};
}
Aqui o segundo pedaço pode ficar vazio: isso é normal e útil. Por exemplo, key= resulta em valor vazio à direita.
Mini-teste em main:
#include <iostream>
#include <string_view>
#include <utility>
int main() {
auto [left, right] = split_once("key=value", '=');
std::cout << left << " | " << right << '\n'; // key | value
}
Uma palavra sobre evolução do padrão: nas discussões do comitê às vezes aparecem pontos sobre como string e string_view devem interoperar melhor. Ou seja, "strings sem cópias" não é apenas um truque raro, é uma direção de evolução da biblioteca padrão para maior conveniência.
remove_prefix() e remove_suffix(): movendo limites sem nova string
Uma tarefa muito comum ao ler comandos ou dados é remover espaços nas bordas. Com std::string você poderia usar substr() criando novas strings, ou manipular índices cuidadosamente. std::string_view permite algo mais elegante: apenas mover a janela de visualização. Os caracteres não mudam, não há alocação — você só diz: “veja não o texto inteiro, mas seu meio”.
Vamos escrever trim_spaces, que remove espaços nas extremidades:
#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;
}
Testemos:
#include <iostream>
#include <string_view>
int main() {
std::cout << '[' << trim_spaces(" hi ") << "]\n"; // [hi]
}
Importante: trim_spaces não "cria" nada. Ela retorna um novo string_view que aponta para o texto original, apenas com limites diferentes.
Isso é precisamente a razão pela qual string_view é tão bom em parâmetros: você aceita a entrada como uma visão, normaliza rapidamente movendo a janela, e então realiza parsing no fragmento resultante — tudo sem cópias.
3. Exemplo: parsing de comando sem cópias, armazenamento com cópia
Para que os exemplos não fiquem "no vácuo", vamos continuar desenvolvendo nosso app de console didático. Imagine que temos uma simples "lista de tarefas": lemos uma linha de comando como "add Comprar leite", e colocamos a tarefa em um std::vector<std::string>. Sem modelos complexos ou status — só texto, para não complicar.
O objetivo é claro: parsear a linha do comando sem cópias desnecessárias, mas ao adicionar a tarefa — copiar para std::string, pois não se pode armazenar um string_view "para sempre" (ele não possui a memória).
parse_command: comando e argumento como string_view
Vamos criar uma função que de uma linha como "add Comprar leite" extrai o comando e o argumento:
#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)};
}
Note: tanto trim_spaces quanto split_once operam sobre string_view, ou seja, não criam strings novas.
Agora um trecho de main: lemos a linha, fazemos parse e reagimos.
#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 (se entrou "add something")
}
Aqui está a ideia prática principal: std::string_view é ideal para "ler e analisar", mas quando você quer salvar o resultado independente da string original, você honestamente transforma em std::string. No código isso aparece literalmente: tasks.push_back(std::string(arg)).
Se desenvolvermos para um mini-loop, vira algo parecido com uma utilidade CLI "de verdade" (divido em pedaços pequenos para não transformar o código em uma sopa):
#include <iostream>
#include <string>
bool read_line(std::string& line) {
std::cout << "> ";
return static_cast<bool>(std::getline(std::cin, line));
}
E o tratamento de alguns comandos:
#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';
}
Repare como os contratos se combinam bem: handle recebe cmd e arg como string_view (para leitura rápida), enquanto tasks armazena std::string (propriedade). É a "preguiça arquitetural correta": economizamos cópias onde não são necessárias e fazemos a cópia quando imprescindível.
Uma tradução humorística para linguagem humana: string_view é "olhar na geladeira do vizinho", e std::string é "comprar os mantimentos e colocar na sua geladeira". Você até pode guardar um bilhete "tem linguiça na prateleira de cima do vizinho" como único plano alimentar, mas não é recomendável.
4. Erros típicos ao usar std::string_view em parâmetros
Erro nº1: aceitar std::string_view e depois armazená-lo "para depois".
A armadilha mais traiçoeira é que string_view parece uma string, e a mão tende a colocá-lo em um std::vector ou como campo de um objeto. Mas string_view não possui a memória: se a string original mudar ou sumir, seu view salvo começará a apontar para "lugar nenhum". Hábito seguro: use string_view como parâmetro e variável local; para armazenar copie para std::string.
Erro nº2: esquecer de checar npos após find() e imediatamente chamar substr().
Código como auto pos = s.find('='); return s.substr(pos + 1); parece compacto, mas quebra quando não há '='. O ritual correto é um pouco mais verboso, mas não transforma a entrada do usuário numa roleta russa: primeiro verificar pos == std::string_view::npos, então substr().
Erro nº3: acessar s[0] ou s[s.size()-1] sem checar empty().
Mesmo que "pela lógica" uma string vazia não deva chegar, ela sempre chegará — porque o usuário apertará Enter, porque o arquivo acabou, porque o sistema de testes decidiu pregar uma peça. O estilo defensivo é simples: antes de usar front()/back()/operator[] faça if (s.empty()) …
Erro nº4: tentar "modificar a string" via std::string_view.
string_view não é um editor, é uma janela. Os métodos remove_prefix/remove_suffix mudam apenas os limites da visão, não os caracteres. Se você precisa modificar texto (substituições, inserções, construir resultado), precisa de um proprietário — geralmente std::string.
Erro nº5: pensar que const std::string& é sempre tão eficiente quanto string_view.
Para passar uma std::string já existente ambos são bons. Mas se você quer aceitar literais e pedaços de string sem objetos intermediários, std::string_view costuma ser mais conveniente: amplia o conjunto de entradas aceitas e reduz a chance de temporárias std::string criadas nos pontos de chamada.
GO TO FULL VERSION