Adeus NULL, Bem-vindo à Dedução — nullptr e auto

Adeus NULL, Bem-vindo à Dedução — nullptr e auto

Dois recursos pequenos em aparência e presentes em quase toda linha de C++ moderno. O nullptr corrige um defeito do NULL que quase ninguém percebia: ele era um inteiro disfarçado, e atrapalhava a escolha entre funções sobrecarregadas. O auto deduz o tipo — e o artigo é honesto sobre quando ele atrapalha.
Linguagem C++

9 min de leitura

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 nullptr e do tipo std::nullptr_t, com a justificativa da distinção em relação a 0.
  • cppreference.com/w/cpp/language/auto: as regras precisas de dedução do auto, incluindo o descarte de referências e const.
  • Scott Meyers, Effective Modern C++ (2014), Itens 5 e 6 ("Prefer auto to explicit type declarations" e suas exceções): a discussão mais completa sobre quando auto ajuda e quando trai.
  • Scott Meyers, Effective Modern C++, Item 8 ("Prefer nullptr to 0 and NULL"): o argumento canônico para a troca que adotamos.
  • ISO C++ Core Guidelines, regras ES.11 (uso de auto) e ES.47 ("Use nullptr rather than 0 or NULL"): 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: - aint - bdouble - cconst 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. - dchar - ebool

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

Comentários

Mais em Linguagem C++

A Sabedoria Codificada — Core Guidelines e Código Limpo em C++
A Sabedoria Codificada — Core Guidelines e Código Limpo em C++

As Core Guidelines destilam experiência de quem projetou a linguagem, mas…

Um Método, Vários Comportamentos — Funções Virtuais e Polimorfismo
Um Método, Vários Comportamentos — Funções Virtuais e Polimorfismo

Uma função que recebe uma forma qualquer e manda desenhá-la, sem saber se é…

Nascimento e Morte de um Objeto — Construtores, Destrutores e o RAII na Prática
Nascimento e Morte de um Objeto — Construtores, Destrutores e o RAII na Prática

Uma classe que adquire um recurso ao nascer e o devolve ao morrer, sem um…