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 inclueminvalid_argument,out_of_range,domain_errorelength_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 incluemrange_error,overflow_erroreunderflow_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.