Exceções e construtores

De ccppbrasil.org

O objetivo deste artigo é esclarecer algumas dúvidas sobre exceções lançadas na contrução de uma classe.
O assunto tratamente de erros é muito interessante, mas o enfoque aqui vai ser um pouco mais retrito para o conteúdo não ficar muito grande.

Resumo do mecanismo dos construtores:

  • A construção de um objeto define a sua validade. Após criado, um objeto deve estar num estado válido.
  • O destructor de um objeto só será chamado caso a sua construção tenha sucesso. Um objeto só tem sucesso na construção após o termino do seu constructor.
  • Um objeto criado dinamicamente com new, só tem seu constructor chamado caso o new tenha sucesso. Caso o new não tenha sucesso o contructor do objeto nunca será chamado incluindo classe base e objetos internos. Por sua vez o destructor também não. Nenhuma memória foi alocada e nenhum operador delete será chamado.
  • Se o operador new tiver sucesso após a memória alocada será chamado o constructor do objeto. Se por ventura o contructor do objeto lançar uma exceção todos os destructors dos objetos integrantes e dos objetos base que já foram construídos até o momento serão chamados. O constructor do objeto a ser criado foi chamado mas não concluído com sucesso consequentemente o destructor nunca será chamado. A memória criada pelo operador new será liberada automaticamente pelo operador delete. (Não confunda operador delete com destructor)
  • Se algum dos objetos integrantes do objeto a ser criado ou objetos base lançarem uma exceção todos os destructors dos objetos integrantes e das dos objetos base que foram criados com sucesso até o momento serão chamados. Os demais constructors dos objetos base e integrantes não serão chamados. O contructor do objeto sendo criado também nunca será chamado e consequentemente o destructor também não. A memória allocada pelo operador new (não confundir com constructor) será liberada com a chamada do operador delete.


Como proceder.
A seguir primeiro o que não deve ser feito e porque.

struct A {
  A() {
    cout << "A ctor" << endl;
  }
  ~A() {
    cout << "~A" << endl;
  }
};

struct B {
  B() {
    cout << "B ctor" << endl;
    throw std::exception("throw in B");
  }
  ~B() {
    cout << "~B" << endl;
  }
};

struct C {
  A * a;
  B * b;
  C() : a(new A()) , b(new B()) //péssima ideia
{
}

A questão é que se houver uma exceção em "b", o construtor de C nunca será chamado. O ponteiro de "a" não é um tipo que tenha destructor e nunca será chamado. Neste caso haverá um memory leak na memória alocada em "a";
Outro exemplo:

struct C {
  A  a;
  B  b;
  C() 
  {
  }
{
}

Este exemplo não tem nenhum problema. O que ocorre: O contructor de "a" será chamado e terá sucesso. O contructor de "b" será chamado e lança uma exceção. Neste caso o contructor de C nunca será chamado. O destructor de "a" será chamado normalmente já que a contrução de "a" teve sucesso.
O mesmo exemplo pode ser feito usando com classes base.

Agora corrigindo o primeiro exemplo:

struct C {
  A * a;
  B * b;
  C() : a(0) , b(0) //inicialize
  {
    try
    {
      a = new A();
      b = new B();
    }
    catch(std::exception & e)
    {
      if (a)
        delete a;
      if (b)
        delete b;
      throw;
    }
  }
};

melhor ainda..

struct C {
  A * a;
  B * b;
  C()
  {
    std::auto_ptr<A> sp1(new A());
    std::auto_ptr<B> sp2(new B());
    a = sp1.release();
    b = sp2.release();
  }
};

Os ponteiros de "a" e "b" são inicializados com null. A construção de "a" e "b" foi protegida com o uso de try catch. Neste caso só estou protegendo contra (std::exception) mas poderia usar um catch(...) e pegar qualquer exceção.

Quando a exceção for lançada, neste exemplo especificamente, o objeto "a" já foi criado. O objeto "b" não. Preciso liberar todos os recursos alocados no momento pois em caso de não concluir a construção de C seu destructor nunca será chamado e preciso fazer com que C não tenha responsabilidade sobre nenhum recurso.
Depois de feita a limpeza eu lanço novamente a mesma exeção pois meu objeto não será válido caso eu não possua "a" e "b" corretamente. Poderia existir um caso em que eu deixasse o objeto C parcialmente correto. Não é uma boa prática, mas em algum casos é preferivel funcionar "meia boca" do que não fazer nada. O usário não fica muito satisfeito com isto, mas poderia, por exemplo aparecer uma mensagem: "O objeto não pode ser contruido totalmente e algumas funcionalidades ficaram desabilitadas, deseja processeguir?". Bom caso fosse esta situação basta não colocar o throw. O destructor seria chamado normalmente pois o objeto será considerado contruído.


O comportamente definido para o new no C++ é lançar exceção em caso de falha. Quando está se interfaceando com programas cujo tratamente de erros não é baseado em exceções, ou você está usando dll's você deve usar outra abordagem. Por exemplo, você tem uma dll que aloca um objeto e retorna uma interface. Neste caso você não quer que exceções "vazem" entre os módulos. E realmente você não deve deixar isto acontecer.

Interface * GetInterface()
{
  MyObject * p new MyObject();// MyObject implementa a classe abstrata Interface
  return p;
}

Para evitar o vazamento da exceção você poderia usar um new ( nothrow ). É uma variação do operator new que não gera exceção, retorna 0 (null) ao invés disto.

  • Falando especificamente do visual C++ o comportamento até a versão 6.0 era retornar null sempre em caso de erro o que não seguia o padrão.
Interface * GetInterface()
{
  MyObject * p new (nothrow)MyObject();// MyObject implementa a classe abstrata Interface
  return p;
}

Agora a função GetInterface fica definida assim. Retornarei um ponteiro para a interface ou retornarei null em caso de erro na alocação de memória. Um detalhe é que o notrow se refere apenas a alocação de memória então se por acaso MyObject possa lançar uma exeção no contructor então você teria que implementar assim:

Interface * GetInterface()
{
try
{
  MyObject * p new (nothrow)MyObject();// MyObject implementa a classe abstrata Interface
  return p;
}
catch(...)
{
  return null;
}
}

A função diria o seguinte. Retorno o ponteiro da interface ou null caso tenha havido algum erro na contrução ou alocação de memória.

Este texto não abordou todos os aspectos ainda, mas dá uma dica de como proceder na criação adequada e como proceder para fazer o tratamente de exceções em constructors. O mecanismo de exceções está presente em toda parte no C++. Quando você usa STL e está fazendo um puch_back por exemplo pode estar ocorrendo uma alocação de memória com new. É preciso saber que funções podem lançar e exceção e quais não lançam. Quando estiver criando um framework com suas próprias funções a documentação é importantíssima.

No C++ quando uma função é descrita com throw( ) no final indica que ela não lança exceções. Este é um longo assunto, portanto, para maiores detalhes procure a documentação de seu compilador e da implementação da STL que está utilizando.

Nas versões mais recentes do livro TCPL (The C++ Programming Language ou A Linguagem de Programação C++) tem a descrição das exceções que podem ocorrer na STL. Por enquanto é isso, fico a disposição para discutir este tema.

Thiago R. Adams 14/01/2006
Original: http://paginas.terra.com.br/informatica/thiago_adams/exceptionsctor_port.htm


Veja também: Aquisição de recurso é inicialização

Ferramentas pessoais