Como projetar tendo em vista a segurança da exceção

Uma das vantagens do mecanismo de exceção é que a execução, juntamente com os dados sobre a exceção, leva diretamente da instrução que lança a exceção para a primeira instrução catch que a manipula. O manipulador pode ser qualquer número de níveis na pilha de chamadas. As funções que são chamadas entre a instrução try e a instrução throw não são necessárias para saber nada sobre a exceção gerada. No entanto, elas precisam ser projetadas para que possam sair do escopo "inesperadamente" a qualquer momento em que uma exceção possa se propagar desde a base, e fazê-lo sem deixar para trás objetos parcialmente criados, memória vazada ou estruturas de dados em estados inutilizáveis.

Técnicas básicas

Uma política robusta de tratamento de exceções requer consideração cuidadosa e deve fazer parte do processo de design. Em geral, a maioria das exceções são detectadas e lançadas nas camadas inferiores de um módulo de software, mas normalmente essas camadas não têm contexto suficiente para lidar com o erro nem expor uma mensagem aos usuários finais. Nas camadas intermediárias, as funções podem capturar e relançar uma exceção quando precisam inspecionar o objeto de exceção ou elas têm informações úteis adicionais para fornecer para a camada superior que vai capturar a exceção. Uma função deve capturar e "engolir" uma exceção somente se for capaz de se recuperar completamente dela. Em muitos casos, o comportamento correto nas camadas intermediárias é permitir que uma exceção se propague na pilha de chamadas. Mesmo na camada mais alta, talvez seja apropriado permitir que uma exceção sem tratamento encerre um programa se a exceção deixar o programa em um estado no qual sua correção não possa ser garantida.

Não importa como uma função lida com uma exceção, para ajudar a garantir que ela seja "segura nas exceções", ela deve ser projetada de acordo com as regras básicas a seguir.

Manter as classes de recursos simples

Ao encapsular o gerenciamento manual de recursos nas classes, use uma classe que não faça nada além de gerenciar um único recurso. Mantendo a classe simples, você reduz o risco de introduzir vazamentos de recursos. Use ponteiros inteligentes sempre que possível, conforme mostrado no exemplo a seguir. Este exemplo é intencionalmente artificial e simplista para realçar as diferenças ao usar shared_ptr.

// old-style new/delete version
class NDResourceClass {
private:
    int*   m_p;
    float* m_q;
public:
    NDResourceClass() : m_p(0), m_q(0) {
        m_p = new int;
        m_q = new float;
    }

    ~NDResourceClass() {
        delete m_p;
        delete m_q;
    }
    // Potential leak! When a constructor emits an exception,
    // the destructor will not be invoked.
};

// shared_ptr version
#include <memory>

using namespace std;

class SPResourceClass {
private:
    shared_ptr<int> m_p;
    shared_ptr<float> m_q;
public:
    SPResourceClass() : m_p(new int), m_q(new float) { }
    // Implicitly defined dtor is OK for these members,
    // shared_ptr will clean up and avoid leaks regardless.
};

// A more powerful case for shared_ptr

class Shape {
    // ...
};

class Circle : public Shape {
    // ...
};

class Triangle : public Shape {
    // ...
};

class SPShapeResourceClass {
private:
    shared_ptr<Shape> m_p;
    shared_ptr<Shape> m_q;
public:
    SPShapeResourceClass() : m_p(new Circle), m_q(new Triangle) { }
};

Usar a linguagem RAII para gerenciar recursos

Para ser segura na exceção, uma função deve garantir que os objetos alocados por ela usando malloc ou new sejam destruídos e todos os recursos, como identificadores de arquivo, sejam fechados ou liberados mesmo que uma exceção seja lançada. A linguagem RAII (Inicialização de Recursos é Inicialização) vincula o gerenciamento desses recursos ao tempo de vida das variáveis automáticas. Quando uma função sai do escopo, retornando normalmente ou devido a uma exceção, os destruidores de todas as variáveis automáticas totalmente construídas são invocados. Um objeto wrapper da RAII, como um ponteiro inteligente, chama a função de exclusão ou fechamento apropriada em seu destruidor. No código com segurança na exceção, é extremamente importante passar a propriedade de cada recurso imediatamente para algum tipo de objeto da RAII. Observe que as classes vector, string, make_shared, fstream e as classes semelhantes lidam com a aquisição do recurso para você. No entanto, unique_ptr e as construções tradicionais shared_ptr são especiais porque a aquisição de recursos é executada pelo usuário e não pelo objeto; portanto, elas contam como Liberação de Recurso é Destruição, mas são questionáveis como RAII.

As três garantias de exceção

Normalmente, a segurança na exceção é discutida nos termos de três garantias de exceção que uma função pode fornecer: a garantia à prova de falhas, a garantia forte e a garantia básica.

Garantia de não falhar

A garantia de não falhar (ou"no-throw") é a garantia mais forte que uma função pode fornecer. Ela declara que a função não lançará nem permitirá que uma exceção se propague. No entanto, você não pode fornecer essa garantia de modo confiável, a menos que (a) você saiba que todas as funções que essa função chama também sejam livres de falha, ou (b) você saiba que todas as exceções lançadas são capturadas antes de alcançarem essa função, ou (c) você saiba como capturar e lidar corretamente com todas as exceções que possam chegar a essa função.

Tanto a garantia forte quanto a garantia básica dependem da suposição de que os destruidores sejam livres de falha. Todos os contêineres e tipos na Biblioteca Padrão garantem que seus destruidores não lancem exceções. Há também um requisito inverso: a Biblioteca Padrão exige que os tipos definidos pelo usuário que são fornecidos a ela, por exemplo, como argumentos de modelo, devem ter destruidores que não lancem exceções.

Garantia forte

A garantia forte indica que, se uma função sair do escopo devido a uma exceção, ela não vazará memória e o estado do programa não será modificado. Uma função que fornece uma garantia forte é essencialmente uma transação que tem semântica de confirmação ou reversão: ela tem êxito completo ou não tem nenhum efeito.

Garantia básica

A garantia básica é a mais fraca das três. No entanto, pode ser a melhor opção quando uma garantia forte é muito cara no consumo de memória ou no desempenho. A garantia básica indica que, se ocorrer uma exceção, nenhuma memória será vazada e o objeto permanecerá em um estado utilizável, embora os dados possam ter sido modificados.

Classes com segurança de exceção

Uma classe pode ajudar a garantir sua própria segurança de exceção, impedindo que seja parcialmente construída ou parcialmente destruída, mesmo quando é consumida por funções não seguras Se um construtor de classe for encerrado antes da conclusão, o objeto nunca será criado e seu destruidor nunca será chamado. Embora variáveis automáticas inicializadas antes da exceção tenham seus destruidores invocados, a memória alocada dinamicamente ou os recursos que não são gerenciados por um ponteiro inteligente ou variável automática semelhante serão vazados.

Os tipos internos são todos livres de falha e os tipos da Biblioteca Padrão dão suporte à garantia básica no mínimo. Siga estas diretrizes para qualquer tipo definido pelo usuário que deva ser seguro nas exceções:

  • Use ponteiros inteligentes ou outros wrappers do tipo RAII para gerenciar todos os recursos. Evite a funcionalidade de gerenciamento de recursos no destruidor de classe, pois o destruidor não será invocado se o construtor gerar uma exceção. No entanto, se a classe for um gerenciador de recursos dedicado que controla apenas um recurso, será aceitável usar o destruidor para gerenciar recursos.

  • Entenda que uma exceção lançada em um construtor de classe base não pode ser engolida em um construtor de classe derivada. Se você quiser converte e lançar novamente a exceção de classe base em um construtor derivado, use um bloco try na função.

  • Considere a possibilidade de armazenar todo o estado da classe em um membro de dados que esteja encapsulado em um ponteiro inteligente, especialmente se uma classe tiver um conceito de "inicialização que tem permissão para falhar". Embora o C++ permita membros de dados não inicializados, ele não dá suporte a instâncias de classe não inicializadas ou parcialmente inicializadas. Um construtor deve ter êxito ou falhar; nenhum objeto será criado se o construtor não for executado até a conclusão.

  • Não permita que exceções escapem de um destruidor. Um axioma básico do C++ é que os destruidores nunca devem permitir que uma exceção se propague para o topo da pilha de chamadas. Se um destruidor tiver que executar uma operação que tem potencial para lançamento de exceção, ele deverá fazê-lo em um bloco try catch e engolir a exceção. A biblioteca padrão fornece essa garantia em todos os destruidores que ela define.

Confira também

Práticas recomendadas do C++ modernas para tratamento de erros e exceções
Como realizar a interface entre códigos excepcionais e não excepcionais