Menu

Exceções em C++: throw, what() e tratamento de erros

Exceções relatam erros que uma função não consegue tratar localmente. Aprenda a usar throw, quais são os tipos de exceção padrão, a mensagem do what() e por que exceções são melhores que códigos de retorno para as falhas que realmente importam.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Por que as exceções existem

Na página anterior você usou enum class para dar nomes significativos aos estados de erro. Isso é ótimo para resultados que uma função espera e que quem a chama deveria inspecionar. Mas algumas falhas são diferentes: uma função no fundo da sua pilha de chamadas descobre que um arquivo não abre, ou que um argumento não faz sentido, e ela não tem ideia do que o programa deveria fazer a respeito. Retornar um código de erro só funciona se cada chamador da cadeia lembrar de verificá-lo e propagá-lo para cima. Perca uma única verificação e o programa segue navegando com lixo.

As exceções resolvem isso. Quando algo dá errado, você lança um objeto com throw. A execução para imediatamente, a pilha é desempilhada (todo objeto local entre o throw e o tratador tem seu destrutor executado) e o controle salta para o catch correspondente mais próximo. Uma exceção não tratada não pode ser ignorada silenciosamente: se nada a capturar, o programa chama std::terminate e aborta.

Esta página foca no lado do lançamento: os próprios objetos de erro. A próxima página aprofunda na mecânica do try/catch em detalhes.

O lançamento e a mensagem do what()

Tecnicamente você pode lançar (throw) qualquer valor —throw 42; ou throw "oops"; são legais— mas não faça isso. A convenção que todo mundo segue é lançar um objeto derivado de std::exception. Essa classe base declara um único método virtual, what(), que retorna uma descrição const char* do problema. Seguir a convenção significa que um único catch (const std::exception& e) pode tratar qualquer coisa.

O cabeçalho <stdexcept> te dá tipos prontos cujo construtor recebe a mensagem:

Repare que what() retorna exatamente a string com a qual você construiu a exceção. Repare também que a capturamos por const exception& mesmo tendo lançado um runtime_error: isso funciona porque runtime_error é um std::exception (uma relação que você reconhecerá da página sobre herança).

A hierarquia de exceções padrão

Antes de escrever seu próprio tipo de exceção, verifique se a biblioteca padrão já tem um que sirva. Todos herdam de std::exception e se dividem em duas famílias em <stdexcept>:

  • logic_error — um defeito na lógica do programa que poderia, em princípio, ser detectado antes de executar. Os subtipos incluem invalid_argument, out_of_range, domain_error e length_error.
  • runtime_error — uma falha que só aparece em tempo de execução e não é um erro de programação em si. Os subtipos incluem range_error, overflow_error e underflow_error.

Muitas funções de biblioteca lançam essas exceções por você. Por exemplo, std::vector::at() faz verificação de limites e lança out_of_range em vez de deixar você ler além do fim:

Esse at() é a contraparte segura do v[9]. O operator[] simples não faz verificação de limites: ler v[9] aqui é comportamento indefinido, não uma exceção. Escolher at() é a forma de transformar uma corrupção silenciosa em um erro capturável.

Escolha o tipo que descreve o erro: invalid_argument quando quem chama passa algo sem sentido, out_of_range para problemas de índice/chave, runtime_error para "o mundo externo falhou comigo".

Escrevendo seu próprio tipo de exceção

Quando nenhum tipo padrão serve —você quer anexar dados extras, ou capturar (catch) especificamente o seu erro e mais nada— defina uma classe que herde de std::exception (ou de um de seus subtipos) e sobrescreva what(). Herdar de std::runtime_error é o caminho mais fácil porque ele já armazena a mensagem e implementa what() por você:

Como NetworkError carrega um código de status, o tratador pode reagir a ele: tentar de novo em um 5xx, desistir em um 4xx. Uma simples string de erro não conseguiria fazer isso. O tipo personalizado também permite que um catch (const NetworkError&) capture apenas os problemas de rede e deixe todo o resto para o tratador mais geral abaixo dele.

Se você algum dia herdar diretamente de std::exception (não de runtime_error), lembre-se de sobrescrever what() você mesmo e marcá-lo como noexcept para combinar com a assinatura da base:

class ParseError : public std::exception {
public:
    const char* what() const noexcept override {
        return "failed to parse input";
    }
};

Lance por valor, capture por referência

Esta é a regra mais importante das exceções em C++, e a que iniciantes erram. Lance objetos por valor e capture-os por referência const.

throw runtime_error("oops");            // por valor - correto
catch (const runtime_error& e) { ... }  // por referência const - correto

Capturar por valor em vez disso —catch (std::exception e)— copia a exceção para um objeto da classe base e fatia a parte derivada. Após o fatiamento, e.what() chama a implementação da base, não a sua sobrescrita, então a sua mensagem cuidadosamente elaborada desaparece:

try {
    throw NetworkError(503, "service unavailable");
} catch (std::exception e) {       // por valor - fatiamento de objeto!
    std::cout << e.what();         // mensagem genérica, status() sumiu
}

A referência (&) preserva o tipo dinâmico real, de modo que o what() virtual é despachado corretamente e você ainda consegue acessar os membros derivados. Adicione const porque você está apenas lendo a exceção, não a modificando. Nunca lance um ponteiro (throw new runtime_error(...)): quem a capturar teria que fazer delete nele, e em qual caminho de código? Esse é exatamente o vazamento que as exceções deveriam evitar.

Próximo: try-catch

Agora você já sabe criar e lançar (throw) exceções bem formadas e escolher o tipo padrão certo para cada falha. A outra metade da história é o lado da captura. A próxima página cobre try/catch por completo: ordenar vários blocos catch do mais específico ao mais geral, o pega-tudo catch (...), o relançamento com um throw; puro e como o RAII (lembre dos ponteiros inteligentes) garante que seus recursos sejam liberados enquanto a pilha é desempilhada.

Perguntas frequentes

O que é uma exceção em C++?

Uma exceção é um objeto que sinaliza um erro que a função atual não consegue tratar sozinha. Você a lança com throw, a pilha é desempilhada (destruindo os objetos locais pelo caminho) e um bloco catch correspondente mais acima assume o controle. Isso separa o código que detecta um problema do código que decide o que fazer a respeito.

Qual é a diferença entre throw e return para erros?

Um valor de return precisa ser verificado por quem chamou, e é fácil esquecer disso: o programa simplesmente segue em frente com dados ruins. Uma exceção lançada não pode ser ignorada: se ninguém a capturar, o programa termina. Exceções são para falhas genuínas (um arquivo não abre, a entrada é inválida); valores de retorno continuam sendo o certo para resultados comuns, incluindo casos esperados de "não encontrado".

O que o método what() faz nas exceções de C++?

Toda classe derivada de std::exception fornece um método virtual what() que retorna um const char* descrevendo o erro. Quando você captura uma exceção, chamar e.what() te dá a mensagem legível que você pode registrar ou imprimir. Os tipos de exceção padrão a definem a partir da string que você passa ao construtor deles.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR