Melhores práticas modernas recomendadas do C++ para tratamento de erros e exceções

No C++moderno, na maioria dos cenários, a maneira preferida de relatar e lidar com erros lógicos e de tempo de execução é usar exceções. É especialmente verdadeiro quando a pilha pode conter várias chamadas de função entre a função que detecta o erro e a função que tem o contexto para lidar com o erro. As exceções fornecem uma maneira formal e bem definida para o código que detecta erros para passar as informações para a pilha de chamadas.

Usar exceções para código excepcional

Os erros do programa são frequentemente divididos em duas categorias:

  • Erros lógicos causados por erros de programação. Por exemplo, um erro de “índice fora do intervalo”.
  • Erros de tempo de execução que estão além do controle do programador. Por exemplo, um erro de “serviço de rede indisponível”.

Na programação no estilo C e no COM, o relatório de erros é gerenciado retornando um valor que representa um código de erro ou um código de status para uma função específica ou definindo uma variável global que o chamador pode recuperar opcionalmente após cada chamada de função para ver se erros foram relatados. Por exemplo, a programação COM usa o valor de retorno HRESULT para comunicar erros ao chamador. E a API Win32 possui a função GetLastError para recuperar o último erro relatado pela pilha de chamadas. Em ambos os casos, cabe ao chamador reconhecer o código e respondê-lo adequadamente. Se o chamador não tratar explicitamente o código de erro, o programa poderá falhar sem aviso. Ou, ele poderá continuar a ser executado usando dados incorretos e produzir resultados incorretos.

As exceções são preferenciais no C++ moderno pelos seguintes motivos:

  • Uma exceção força o código de chamada a reconhecer uma condição de erro e lidar com ela. Exceções sem tratamento interrompem a execução do programa.
  • Uma exceção salta para o ponto na pilha de chamadas que pode lidar com o erro. Funções intermediárias podem permitir a propagação da exceção. Elas não precisam ser coordenadas com outras camadas.
  • O mecanismo de desenrolamento de pilha de exceção destrói todos os objetos no escopo depois que uma exceção é lançada, de acordo com regras bem definidas.
  • Uma exceção permite uma separação limpa entre o código que detecta o erro e o código que manipula o erro.

O exemplo simplificado a seguir mostra a sintaxe necessária para lançar e capturar exceções em C++:

#include <stdexcept>
#include <limits>
#include <iostream>

using namespace std;

void MyFunc(int c)
{
    if (c > numeric_limits< char> ::max())
    {
        throw invalid_argument("MyFunc argument too large.");
    }
    //...
}

int main()
{
    try
    {
        MyFunc(256); //cause an exception to throw
    }

    catch (invalid_argument& e)
    {
        cerr << e.what() << endl;
        return -1;
    }
    //...
    return 0;
}

Exceções em C++ se assemelham a linguagens como C# e Java. No bloco try, se uma exceção for lançada ela será capturada pelo primeiro bloco catch associado cujo tipo corresponda ao da exceção. Em outras palavras, a execução salta da instrução throw para a instrução catch. Se nenhum bloco de captura utilizável for encontrado, std::terminate será invocado e o programa será encerrado. No C++, qualquer tipo pode ser gerado; no entanto, recomendamos que você gere um tipo que deriva direta ou indiretamente de std::exception. No exemplo anterior, o tipo de exceção, invalid_argument, é definido na biblioteca padrão no arquivo de cabeçalho de <stdexcept>. O C++ não fornece nem requer um bloco finally para garantir que todos os recursos sejam liberados se uma exceção for lançada. A aquisição de recursos é o idioma RAII (inicialização), que usa ponteiros inteligentes, fornece a funcionalidade necessária para a limpeza de recursos. Para obter mais informações, consulte Como fazer o design tendo em vista a segurança para exceções. Para obter informações sobre o mecanismo de desenrolamento de pilha C++, consulte Desenrolamento de exceções e de pilha.

Diretrizes básicas

O tratamento de erros robusto é um desafio em qualquer linguagem de programação. Embora as exceções forneçam vários recursos que dão suporte ao bom tratamento de erros, elas não conseguem fazer todo o trabalho para você. Para obter os benefícios do mecanismo de exceção, tenha exceções em mente ao projetar seu código.

  • Use afirmações para verificar condições que devem ser sempre verdadeiras ou sempre falsas. Use exceções para verificar se há erros que podem ocorrer, por exemplo, erros na validação de entrada em parâmetros de funções públicas. Para obter mais informações, confira a seção Exceções versus instrução assert.
  • Use exceções quando o código que manipula o erro for separado do código que detecta o erro por uma ou mais chamadas de função intervindo. Considere se os códigos de erro devem ser usados em loops críticos de desempenho, quando o código que manipula o erro está estreitamente ligado ao código que o detecta.
  • Para cada função que possa lançar ou propagar uma exceção, forneça uma das três garantias de exceção: a garantia forte, a garantia básica ou a garantia nothrow (noexcept). Para obter mais informações, consulte Como fazer o design tendo em vista a segurança para exceções.
  • Gere exceções por valor, faça catch delas por referência. Não pegue o que você não consegue controlar.
  • Não use especificações de exceção, que estão preteridas no C++11. Para obter mais informações, consulte as Especificações de exceção e a seção noexcept.
  • Use tipos de exceção de biblioteca padrão quando eles se aplicarem. Derive tipos de exceção personalizados da hierarquia de classe exception.
  • Não permita que exceções escapem de destruidores ou funções de desalocação de memória.

Exceções e desempenho

O mecanismo de exceção terá um custo mínimo de desempenho se nenhuma exceção for gerada. Se uma exceção for gerada, o custo da passagem e do desenrolamento da pilha será aproximadamente comparável ao custo de uma chamada de função. Outras estruturas de dados são necessárias para rastrear a pilha de chamadas depois que um bloco try é inserido, e mais instruções são necessárias para desenrolar a pilha se uma exceção for lançada. No entanto, na maioria dos cenários, o custo no desempenho e no volume de memória não é significativo. O efeito adverso das exceções sobre o desempenho provavelmente será significativo apenas em sistemas com restrição de memória. Ou, em loops críticos de desempenho, em que um erro provavelmente ocorrerá regularmente e há uma ligação estreita entre o código para lidar com ele e o código que o relata. De qualquer forma, é impossível saber o custo real das exceções sem criação de perfil e medição. Mesmo nos raros casos em que o custo é significativo, você pode ponderá-lo contra o aumento da correção, a manutenção mais fácil e outras vantagens fornecidas por uma política de exceção bem projetada.

Exceções versus instruções assert

Exceções e instruções assert são dois mecanismos distintos para detectar erros em tempo de execução em um programa. Use instruções assert para testar condições durante o desenvolvimento que devem ser sempre verdadeiras ou sempre falsas se todo o seu código estiver correto. Não faz sentido tratar esse erro usando uma exceção, pois o erro indica que algo no código precisa ser corrigido. Ele não representa uma condição da qual o programa precisa se recuperar em tempo de execução. Uma assert interrompe a execução na instrução, de forma que você possa inspecionar o estado do programa no depurador. Uma exceção continua a execução do primeiro manipulador de catch apropriado. Use exceções para verificar as condições de erro que podem ocorrer em tempo de execução, mesmo se o código estiver correto. Por exemplo, "arquivo não encontrado" ou "memória insuficiente". As exceções podem lidar com essas condições, mesmo que a recuperação apenas gere uma mensagem para um log e termine o programa. Sempre verifique argumentos para funções públicas usando exceções. Mesmo que sua função seja livre de erros, talvez você não tenha controle total sobre os argumentos que um usuário pode passar para ela.

Exceções C++ versus exceções SEH do Windows

Os programas C e C++ podem usar o mecanismo SEH (tratamento de exceção) estruturado no sistema operacional Windows. Os conceitos em SEH se assemelham aos das exceções C++, exceto que o SEH usa os constructos __try, __except e __finally em vez de try e catch. No MSVC (compilador do Microsoft C++), exceções C++ são implementadas para SEH. No entanto, ao escrever código C++, use a sintaxe de exceção C++.

Para obter mais informações sobre SEH, consulte Tratamento de Exceção Estruturada (C/C++).

Especificações de exceção e noexcept

As especificações de exceção foram introduzidas no C++ como uma forma de determinar as exceções que uma função pode gerar. No entanto, as especificações de exceção se mostraram problemáticas na prática e foram preteridas no padrão de rascunho C++11. Recomendamos que você não use especificações de exceção throw, exceto para throw(), o que indica que a função não permite que nenhuma exceção escape. Se você precisar usar especificações de exceção do formulário preterido throw( type-name ), o suporte do MSVC será limitado. Para obter mais informações, confira Especificações de exceção (throw). O especificador noexcept foi introduzido no C++11 como a alternativa preferencial ao throw().

Confira também

Como realizar a interface entre códigos excepcionais e não excepcionais
Referência da linguagem C++
Biblioteca Padrão do C++