De lançar a tratar
Na página anterior você aprendeu a throw (lançar) uma exceção quando algo dá errado. Lançar é apenas metade da história: uma exceção que nunca é capturada chama std::terminate e derruba o seu programa. A instrução try/catch é como você trata o que foi lançado e mantém a execução.
A estrutura é simples: coloque o código arriscado dentro de um bloco try e siga-o com um ou mais blocos catch que reagem a tipos de erro específicos. Se o bloco try executar sem problemas, todos os catch são ignorados. No momento em que algo é lançado, o controle salta direto para o primeiro catch correspondente.
Repare que "after" nunca é impresso. Assim que o throw dispara, o restante do bloco try é abandonado e a execução é retomada dentro do catch correspondente. Depois que o catch termina, o programa continua normalmente abaixo dele.
Capture por referência constante
O hábito mais importante no tratamento de erros em C++: capture exceções por referência const, não por valor.
Capturar por valor copia a exceção e, pior, faz o slicing dela. As exceções padrão formam uma hierarquia (runtime_error e logic_error derivam ambas de std::exception), então capturar uma exceção derivada como um valor base corta a parte derivada. Capturar por referência mantém o objeto intacto e polimórfico:
Aqui lançamos um out_of_range mas o capturamos como const exception&. Como out_of_range deriva de exception, o tratador da classe base corresponde, e a referência faz com que e.what() ainda retorne a mensagem real. Se você tivesse escrito catch (exception e) (por valor), o objeto seria cortado para um simples exception e você poderia perder a mensagem específica.
Vários blocos catch
Um único try pode ser seguido por vários blocos catch, cada um para um tipo de exceção diferente. C++ os testa de cima para baixo e executa o primeiro que corresponder, então ordene-os do mais específico para o mais geral.
Como invalid_argument é mais específico que exception, ele deve vir primeiro. Se você invertesse a ordem e colocasse catch (const exception&) no topo, ele engoliria todas as exceções: o tratador de invalid_argument abaixo dele viraria código morto que nunca executaria. Muitos compiladores avisam sobre isso, mas a linguagem não vai te impedir.
catch (...) e relançamento
Às vezes você quer uma rede de segurança para qualquer coisa que não previu. O tratador genérico catch (...) corresponde a todo tipo de exceção, incluindo as que não derivam de std::exception (alguém pode escrever throw 42; ou throw "oops";).
A desvantagem é que você não recebe nenhum objeto: não há nenhum e para inspecionar. Por isso catch (...) é melhor usado como último recurso: registrar que algo falhou, ou limpar e relançar.
Para relançar a exceção atual (repassá-la a um tratador externo depois de fazer alguma limpeza ou registro local) use um throw; sem operando. Isso preserva a exceção original (seu tipo e mensagem reais), ao contrário de throw e;, que relançaria uma cópia cortada:
O tratador interno registra e relança; o tratador externo em main então lida com ela. Use um throw; sem operando para isso, nunca throw e;.
Desempilhamento e RAII
Quando uma exceção se propaga para fora de um bloco try, C++ realiza o desempilhamento (stack unwinding): cada objeto local entre o throw e o catch correspondente tem seu destrutor chamado, na ordem inversa à da construção. É isso que torna as exceções seguras: os recursos mantidos por objetos da pilha são liberados automaticamente.
É exatamente por isso que você deve manter recursos em tipos RAII (como std::vector, std::string e ponteiros inteligentes) em vez de usar new/delete manuais. Veja o que acontece quando uma exceção atravessa uma alocação manual:
void leaky() {
int* buffer = new int[1000];
mightThrow(); // se isto lançar, a próxima linha nunca executa...
delete[] buffer; // ...e o buffer vaza
}
Como o throw salta por cima do delete[], a memória é perdida. Um ponteiro inteligente resolve isso de graça: seu destrutor executa durante o desempilhamento:
void safe() {
auto buffer = std::make_unique<int[]>(1000);
mightThrow(); // se isto lançar, o destrutor de buffer ainda libera a memória
} // sem delete manual, sem vazamento, mesmo no caminho da exceção
A lição: não tente capturar uma exceção apenas para fazer delete de algo. Deixe os destrutores cuidarem da limpeza e reserve o catch para decidir como se recuperar.
Erros comuns e armadilhas
Um punhado de armadilhas aparece repetidamente:
Não use exceções para o fluxo de controle normal. Lançar e desempilhar é muito mais lento que um simples if. Reserve as exceções para condições de erro genuinamente excepcionais, não para "o usuário digitou uma string vazia".
Um bloco catch vazio esconde bugs. Escrever catch (...) {} para silenciar um erro faz as falhas desaparecerem sem deixar rastro. No mínimo, registre o problema; normalmente você deveria relançá-lo ou tratá-lo adequadamente.
Um destrutor que lança é perigoso. Se um destrutor lança uma exceção durante o desempilhamento (enquanto outra exceção já está em andamento), o programa chama std::terminate. No C++ moderno, os destrutores são implicitamente noexcept: nunca deixe uma exceção escapar de um.
O catch só enxerga o que o try cobre. Uma exceção lançada antes de entrar no try, ou em uma função diferente que não está no caminho de chamadas dentro dele, não será capturada aqui. O catch só protege o código que executa dentro do seu próprio bloco try (diretamente ou nas funções que ele chama).
A seguir: comportamento indefinido
As exceções são a forma definida pela qual C++ avisa que algo deu errado: você lança, você captura, o comportamento é previsível. Mas C++ também tem um canto mais sombrio onde a linguagem não faz nenhuma promessa: desreferenciar um ponteiro pendente, ler além do fim de um array, estouro de inteiro com sinal. A próxima página cobre o comportamento indefinido: o que o desencadeia, por que ele pode parecer "funcionar" até o momento em que deixa de funcionar de forma catastrófica, e como mantê-lo longe do seu código.
Perguntas frequentes
Como o try-catch funciona em C++?
Você coloca o código que pode lançar uma exceção dentro de um bloco try { }. Se uma exceção for lançada, o programa para de executar o restante do bloco try e salta para o primeiro bloco catch correspondente, onde você trata o erro. Se nada for lançado, os blocos catch são totalmente ignorados.
Por que você deve capturar exceções por referência constante em C++?
Capturar por referência (catch (const std::exception& e)) evita copiar o objeto da exceção e, o mais importante, preserva o polimorfismo, de modo que uma exceção derivada capturada como seu tipo base ainda chama o what() correto. Capturar por valor (catch (std::exception e)) corta a parte derivada e pode perder informações.
Como capturar qualquer exceção em C++?
Use catch (...): as reticências capturam qualquer exceção, independentemente do tipo. É um tratador útil como último recurso, mas como você não recebe nenhum objeto para inspecionar, coloque-o depois dos seus blocos catch específicos e use-o principalmente para registrar o erro ou relançá-lo.