O artigo O Fim do char — Textos de Verdade com std::string* encerrou a espinha dorsal da Fase 1, mas deixei dois convidados esperando na antessala. O primeiro é o nullptr, que veio para corrigir um defeito antigo do C que você provavelmente nunca percebeu como defeito: o NULL. O segundo é o auto, a palavra que deixa o compilador deduzir o tipo de uma variável por você. São dois recursos pequenos em aparência, mas que aparecem em praticamente toda linha de C++ moderno. Amarrá-los fecha a fase com elegância e prepara o terreno para a Fase 2, onde tipos ficam mais complexos e escrever seus nomes por extenso viraria um fardo.
O problema do NULL que você herdou do C
Em C, NULL é, na prática, o número zero — tipicamente definido como 0 ou ((void*)0). Durante décadas funcionou, mas em C++ isso cria uma ambiguidade real, porque 0 é um int, e um int pode casar com sobrecargas que você não pretendia:
#include <iostream>
void func(int n) { std::cout << "chamou a versão int: " << n << '\n'; }
void func(char* ptr) { std::cout << "chamou a versão ponteiro\n"; }
int main() {
func(NULL); // Surpresa! NULL é 0, que é int → chama func(int)
// quando você provavelmente queria func(char*)
return 0;
}
Você queria passar "nenhum ponteiro" e acabou chamando a versão int. O nullptr, introduzido no C++11, resolve isso de raiz: ele tem um tipo próprio (std::nullptr_t) que representa "ponteiro nulo" e não é confundível com inteiro:
#include <iostream>
void func(int n) { std::cout << "versão int\n"; }
void func(char* ptr) { std::cout << "versão ponteiro\n"; }
int main() {
func(nullptr); // sem ambiguidade: casa com func(char*), como esperado
return 0;
}
A regra do curso, a partir de agora: nunca use NULL nem 0 para ponteiros; use sempre nullptr. Ele é mais claro para quem lê (grita "isto é um ponteiro nulo") e imune à armadilha de sobrecarga acima. É uma daquelas trocas que não custam nada e só trazem benefício.
int* p = nullptr; // ponteiro que não aponta para nada
if (p == nullptr) { /* ... */ } // teste explícito e legível
if (!p) { /* ... */ } // forma curta equivalente
auto: deixe o compilador escrever o tipo
O segundo convidado ataca outra irritação. Em C, o tipo de cada variável é escrito à mão, e às vezes ele é longo e óbvio ao mesmo tempo. O auto (também do C++11) pede ao compilador que deduza o tipo a partir do valor de inicialização:
#include <iostream>
#include <string>
int main() {
auto i = 42; // i é int
auto x = 3.14; // x é double
auto nome = std::string{"Ana"}; // nome é std::string
auto letra = 'z'; // letra é char
std::cout << i << ' ' << x << ' ' << nome << ' ' << letra << '\n';
return 0;
}
Aqui o auto é pura conveniência — os tipos eram óbvios. O ganho real aparece quando o tipo é verboso. Adiante você vai lidar com tipos como std::vector<std::pair<std::string, int>>::const_iterator. Escrever isso por extenso é ruído; auto deixa o código limpo sem perder tipagem estática, porque o tipo continua fixo e checado — só não precisa ser soletrado:
// Prévia da STL (Fase 4). Não se prenda aos detalhes; foque no auto.
#include <map>
#include <string>
#include <iostream>
int main() {
std::map<std::string, int> idades = {{"Ana", 30}, {"Bruno", 25}};
// Sem auto, o tipo do iterador seria longo e chato de escrever.
// Com auto, fica legível e o compilador cuida do tipo exato.
for (auto it = idades.begin(); it != idades.end(); ++it)
std::cout << it->first << " tem " << it->second << " anos\n";
return 0;
}
A honestidade sobre o auto: quando ele atrapalha
auto é ótimo, mas não é para tudo, e vender isso como bala de prata seria desonesto. Três cuidados.
Primeiro: auto exige inicialização — auto x; não compila, porque não há valor de onde deduzir o tipo. Isso é, na verdade, um benefício: ele impede a variável não inicializada que tanto atormenta em C.
Segundo: auto pode esconder o tipo de quem lê, prejudicando a clareza. auto resultado = calcula(); não diz nada sobre o que resultado é. Quando o tipo importa para o entendimento e não é óbvio pelo lado direito, prefira escrevê-lo. A diretriz é usar auto quando o tipo é longo ou redundante com o lado direito (auto s = std::string{...} — o std::string já está ali), e ser explícito quando o tipo carrega informação que o leitor precisa.
Terceiro, e mais sutil: auto descarta referências e const por padrão. auto sobre uma referência copia o valor. Se você quer uma referência, escreve auto&; se quer uma referência só-leitura, const auto&:
#include <string>
std::string& obtem_ref(); // devolve uma referência a algo
int main() {
auto a = obtem_ref(); // a é std::string — uma CÓPIA!
auto& b = obtem_ref(); // b é std::string& — referência de verdade
const auto& c = obtem_ref(); // c é const std::string& — ref só-leitura
return 0;
}
Esse detalhe volta a importar muito na Fase 4, quando iteramos sobre containers e uma cópia acidental por artigo custa desempenho. Por ora, memorize o par: auto copia, auto&/const auto& referenciam.
O nullptr encerra uma ambiguidade herdada do C: NULL era um inteiro disfarçado e escolhia a sobrecarga errada sem avisar. O auto resolve outro incômodo, o de escrever tipos que o compilador já conhece — e cobra bom senso, porque esconder o tipo numa assinatura pública ou num laço obscuro troca ruído por opacidade.
Esta primeira fase é de tradução: o printf vira stream, o char* vira std::string, a referência aparece onde antes só havia ponteiro, e o const deixa de ser enfeite para virar promessa que o compilador cobra. Nada disso ainda é projetar em C++, mas é o vocabulário sem o qual o resto não se escreve.
Fontes e leituras recomendadas
- cppreference.com/w/cpp/language/nullptr: a especificação de
nullptre do tipostd::nullptr_t, com a justificativa da distinção em relação a0. - cppreference.com/w/cpp/language/auto: as regras precisas de dedução do
auto, incluindo o descarte de referências econst. - Scott Meyers, Effective Modern C++ (2014), Itens 5 e 6 ("Prefer
autoto explicit type declarations" e suas exceções): a discussão mais completa sobre quandoautoajuda e quando trai. - Scott Meyers, Effective Modern C++, Item 8 ("Prefer
nullptrto0andNULL"): o argumento canônico para a troca que adotamos. - ISO C++ Core Guidelines, regras ES.11 (uso de
auto) e ES.47 ("Usenullptrrather than0orNULL"): as diretrizes oficiais sobre ambos.
Exercícios
Exercício 1
Declare um ponteiro para int inicializado com nullptr e escreva um if que só o desreferencia se ele não for nulo. Por que essa checagem é um bom hábito herdado do C e reforçado em C++?
Ver resposta
✓ Resposta: O padrão seguro:
#include <iostream>
int main() {
int* p = nullptr;
if (p != nullptr) { // só entra se p apontar para algo válido
std::cout << *p << '\n';
} else {
std::cout << "ponteiro nulo, nada a mostrar\n";
}
return 0;
}
Desreferenciar um ponteiro nulo é comportamento indefinido tanto em C quanto em C++; a checagem prévia evita o crash. nullptr torna a intenção explícita e legível, e imune às ambiguidades que NULL traria em sobrecargas.
Exercício 2
Para cada linha, diga o tipo deduzido pelo auto:
auto a = 10;
auto b = 10.0;
auto c = "texto"; // cuidado: qual é o tipo de um literal entre aspas?
auto d = 'x';
auto e = true;
Ver resposta
✓ Resposta: Os tipos:
- a → int
- b → double
- c → const char* — a pegadinha! Um literal entre aspas duplas é um arranjo de char que decai para const char*, não um std::string. Para ter std::string, escreva auto c = std::string{"texto"}; ou, em C++14+, auto c = "texto"s; com o sufixo de literal de string.
- d → char
- e → bool
Exercício 3
Explique, com base na aula, por que a chamada abaixo é armadilhosa em C++ e como nullptr a resolve:
void registra(int codigo);
void registra(const char* mensagem);
// registra(NULL); // qual versão é chamada? era essa a intenção?
Ver resposta
✓ Resposta: registra(NULL) é armadilhosa porque NULL vale 0, que é int, e portanto casa com registra(int codigo) — quase certamente não a intenção, que era passar "nenhuma mensagem" para registra(const char*). Com nullptr, a ambiguidade some: registra(nullptr) tem tipo std::nullptr_t, que converte para ponteiro mas não para int, então casa com a versão const char*, como se esperava.
Exercício 4
O trecho a seguir faz uma cópia indesejada. Identifique-a e corrija usando a forma certa de auto, explicando a diferença de desempenho:
#include <string>
#include <vector>
std::vector<std::string> nomes = {"Ana", "Bruno", "Carla"};
for (auto nome : nomes) { // o que acontece com cada elemento aqui?
// ...apenas lê nome...
}
Ver resposta
✓ Resposta: for (auto nome : nomes) copia cada std::string do vetor para a variável nome a cada iteração — desperdício, já que o laço só lê. A correção é usar referência só-leitura:
for (const auto& nome : nomes) { // sem cópia: apenas observa cada elemento
// ...apenas lê nome...
}
A diferença de desempenho: com auto, cada iteração aloca e copia o texto inteiro (custo proporcional ao tamanho da string); com const auto&, nenhuma cópia acontece — nome é um apelido para o elemento do vetor. Em coleções grandes, isso é a diferença entre um laço rápido e um lento. É a regra "auto copia, const auto& referencia" salvando desempenho.
Exercício 5
Considere auto x = obtem(); onde obtem() devolve const std::string&. Que tipo x terá? Se você quisesse evitar a cópia e apenas observar o valor devolvido, como declararia x? Justifique com a regra de dedução.
Ver resposta
✓ Resposta: auto x deduz std::string — uma cópia, porque auto descarta a referência e o const do tipo devolvido. Para observar sem copiar, declare const auto& x = obtem();, que preserva a natureza de referência e a imutabilidade, ligando x diretamente ao objeto referenciado sem duplicá-lo. A regra: auto sozinho sempre produz um tipo por valor; para manter referência/const, escreva-os explicitamente com auto& ou const auto&.