lançando exceções de um destruidor

A maioria das pessoas diz que nunca jogue uma exceção fora do destruidor – isso resulta em um comportamento indefinido. Stroustrup afirma que “o destruidor de vetores invoca explicitamente o destruidor para cada elemento. Isso implica que, se um destrutor de elemento lança, a destruição do vetor falha … Não há realmente nenhuma boa maneira de proteger contra as exceções lançadas pelos destruidores, então a biblioteca não faz garantias se um elemento destruidor lançar “(do Apêndice E3.2) .

Este artigo parece dizer o contrário – que os destruidores jogando são mais ou menos bem.

Então, minha pergunta é a seguinte: se jogar de um destrutor resulta em um comportamento indefinido, como você lida com erros que ocorrem durante um processo de destruição?

Se ocorrer um erro durante uma operação de limpeza, você simplesmente a ignora? Se for um erro que pode potencialmente ser manipulado na pilha, mas não no destruidor, não faz sentido lançar uma exceção fora do destruidor?

Obviamente, esses tipos de erros são raros, mas possíveis.

   

Lançar uma exceção fora de um destruidor é perigoso.
Se outra exceção já estiver sendo propagada, o aplicativo será finalizado.

 #include  class Bad { public: // Added the noexcept(false) so the code keeps its original meaning. // Post C++11 destructors are by default `noexcept(true)` and // this will (by default) call terminate if an exception is // escapes the destructor. // // But this example is designed to show that terminate is called // if two exceptions are propagating at the same time. ~Bad() noexcept(false) { throw 1; } }; class Bad2 { public: ~Bad2() { throw 1; } }; int main(int argc, char* argv[]) { try { Bad bad; } catch(...) { std::cout < < "Print This\n"; } try { if (argc > 3) { Bad bad; // This destructor will throw an exception that escapes (see above) throw 2; // But having two exceptions propagating at the // same time causes terminate to be called. } else { Bad2 bad; // The exception in this destructor will // cause terminate to be called. } } catch(...) { std::cout < < "Never print this\n"; } } 

Isso basicamente se resume a:

Qualquer coisa perigosa (ou seja, que poderia lançar uma exceção) deve ser feita através de methods públicos (não necessariamente diretamente). O usuário da sua class pode, então, manipular potencialmente essas situações usando os methods públicos e capturando possíveis exceções.

O destruidor finalizará o object chamando esses methods (se o usuário não o fez explicitamente), mas qualquer exceção lançada será capturada e descartada (depois de tentar corrigir o problema).

Então, com efeito, você passa a responsabilidade para o usuário. Se o usuário estiver em posição de corrigir exceções, ele chamará manualmente as funções apropriadas e processará os erros. Se o usuário do object não está preocupado (como o object será destruído), então o destruidor é deixado para cuidar dos negócios.

Um exemplo:

std :: fstream

O método close () pode potencialmente lançar uma exceção. O destrutor chama close () se o arquivo foi aberto, mas garante que quaisquer exceções não se propaguem para fora do destruidor.

Portanto, se o usuário de um object de arquivo quiser fazer um tratamento especial para problemas associados ao fechamento do arquivo, ele chamará manualmente o close () e manipulará todas as exceções. Se, por outro lado, eles não se importarem, o destruidor será deixado para lidar com a situação.

Scott Myers tem um excelente artigo sobre o assunto em seu livro "Effective C ++"

Editar:

Aparentemente também em "C ++ Mais Eficaz"
Item 11: Impedir exceções de deixar destruidores

Jogar fora de um destrutor pode resultar em uma falha, porque esse destruidor pode ser chamado como parte do “Desativar Stack”. Pilha desenrolamento é um procedimento que ocorre quando uma exceção é lançada. Neste procedimento, todos os objects que foram colocados na pilha desde o “try” e até a exceção ser lançada, serão terminados -> seus destruidores serão chamados. E durante este procedimento, outro disparo de exceção não é permitido, porque não é possível manipular duas exceções de cada vez, portanto, isso provocará uma chamada para abort (), o programa falhará e o controle retornará ao sistema operacional.

Temos que nos diferenciar aqui, em vez de seguir cegamente as recomendações gerais para casos específicos .

Observe que o seguinte ignora o problema de contêineres de objects e o que fazer em face de vários d tores de objects dentro de contêineres. (E isso pode ser ignorado parcialmente, já que alguns objects não servem para colocar em um contêiner.)

O problema todo se torna mais fácil de pensar quando dividimos as classs em dois tipos. Um professor pode ter duas responsabilidades diferentes:

  • (R) liberar semântica (também conhecida como memory livre)
  • (C) confirmar semântica (também conhecido como flush file to disk)

Se encararmos a questão dessa maneira, então eu acho que pode-se argumentar que a semântica (R) nunca deve causar uma exceção de um dtor, pois há a) nada que possamos fazer sobre isso eb) muitas operações de resources livres não até fornecer verificação de erros, por exemplo, void free(void* p); .

Objetos com semântica (C), como um object de arquivo que precisa liberar seus dados com sucesso ou uma conexão de database (“protegida pelo escopo”) que faz um commit no dtor são de um tipo diferente: Podemos fazer algo sobre o erro o nível de aplicação) e nós realmente não devemos continuar como se nada tivesse acontecido.

Se seguirmos a rota RAII e permitirmos objects que tenham semântica (C) em seus ditadores, acho que também temos que permitir o caso ímpar em que tais ditadores podem lançar. Segue-se que você não deve colocar tais objects em contêineres e também segue que o programa ainda pode terminate() se um committtor for acionado enquanto outra exceção estiver ativa.


No que diz respeito ao tratamento de erros (semântica de confirmação / reversão) e exceções, há uma boa conversa por um Andrei Alexandrescu : tratamento de erros em C ++ / Declarative Control Flow (mantido no NDC 2014 )

Nos detalhes, ele explica como a biblioteca Folly implementa um UncaughtExceptionCounter para suas ferramentas do ScopeGuard .

(Eu deveria notar que os outros também tinham idéias semelhantes.)

Embora a conversa não se concentre em jogar a partir de um d’tor, ela mostra uma ferramenta que pode ser usada hoje para se livrar dos problemas de quando jogar de um dtor.

No futuro , pode haver um recurso padrão para isso, consulte N3614 , e uma discussão sobre isso .

Upd ’17: O recurso std::uncaught_exceptions C ++ 17 para isso é std::uncaught_exceptions afaikt. Vou rapidamente citar o artigo cppref:

Notas

Um exemplo em que int returning uncaught_exceptions é usado é … … primeiro cria um object guarda e registra o número de exceções não-identificadas em seu construtor. A saída é executada pelo destrutor do object de guarda, a menos que foo () seja lançado ( nesse caso, o número de exceções não detectadas no destruidor é maior do que o observado pelo construtor ).

A verdadeira questão de se perguntar sobre o lançamento de um destruidor é “O que o chamador pode fazer com isso?” Existe realmente alguma coisa útil que você possa fazer com a exceção, que compensaria os perigos criados pelo lançamento de um destruidor?

Se eu destruir um object Foo , e o Foo destructor lançar uma exceção, o que eu posso razoavelmente fazer com isso? Eu posso registrá-lo, ou posso ignorá-lo. Isso é tudo. Eu não posso “consertar” isso, porque o object Foo já se foi. Melhor caso, eu registro a exceção e continuo como se nada tivesse acontecido (ou termine o programa). Isso realmente vale potencialmente causar comportamento indefinido jogando de um destruidor?

É perigoso, mas também não faz sentido do ponto de vista de legibilidade / compreensão do código.

O que você tem que perguntar é nessa situação

 int foo() { Object o; // As foo exits, o's destructor is called } 

O que deve pegar a exceção? O chamador de foo? Ou deveria lidar com isso? Por que o chamador de foo se preocupa com algum object interno de foo? Pode haver uma maneira pela qual a linguagem define isso para fazer sentido, mas vai ser ilegível e difícil de entender.

Mais importante, para onde vai a memory do Object? Onde a memory que o object pertence vai? Ainda é alocado (ostensivamente porque o destrutor falhou)? Considere também que o object estava no espaço da pilha , então obviamente desapareceu.

Então considere este caso

 class Object { Object2 obj2; Object3* obj3; virtual ~Object() { // What should happen when this fails? How would I actually destroy this? delete obj3; // obj 2 fails to destruct when it goes out of scope, now what!?!? // should the exception propogate? } }; 

Quando a exclusão de obj3 falha, como eu realmente apago de uma maneira que não falhe? É minha memory caramba!

Agora considere no primeiro trecho de código O object desaparece automaticamente porque está na pilha enquanto o Object3 está no heap. Desde que o ponteiro para Object3 se foi, você é do tipo SOL. Você tem um memory leaks.

Agora, uma maneira segura de fazer as coisas é a seguinte

 class Socket { virtual ~Socket() { try { Close(); } catch (...) { // Why did close fail? make sure it *really* does close here } } }; 

Veja também este FAQ

Do esboço ISO para C ++ (ISO / IEC JTC 1 / SC 22 N 4411)

Assim, os destruidores devem geralmente capturar exceções e não deixá-los se propagar do destruidor.

3 O processo de chamar destrutores para objects automáticos construídos no caminho de um bloco try para uma expressão throw é chamado de “desenrolamento de pilha”. [Nota: Se um destruidor chamado durante o desenrolar de pilha sair com uma exceção, std :: terminate é chamado (15.5.1). Assim, os destruidores devem geralmente capturar exceções e não deixá-los se propagar do destruidor. – nota final

Seu destruidor pode estar executando dentro de uma cadeia de outros destruidores. Lançar uma exceção que não seja detectada pelo chamador imediato pode deixar vários objects em um estado inconsistente, causando ainda mais problemas e ignorando o erro na operação de limpeza.

Todos os outros explicaram porque jogar destruidores são terríveis … o que você pode fazer sobre isso? Se você estiver fazendo uma operação que pode falhar, crie um método público separado que execute a limpeza e possa lançar exceções arbitrárias. Na maioria dos casos, os usuários ignoram isso. Se os usuários quiserem monitorar o sucesso / falha da limpeza, eles podem simplesmente chamar a rotina de limpeza explícita.

Por exemplo:

 class TempFile { public: TempFile(); // throws if the file couldn't be created ~TempFile() throw(); // does nothing if close() was already called; never throws void close(); // throws if the file couldn't be deleted (eg file is open by another process) // the rest of the class omitted... }; 

Como complemento às principais respostas, que são boas, abrangentes e precisas, gostaria de comentar sobre o artigo que você mencionou – aquele que diz “lançar exceções em destruidores não é tão ruim”.

O artigo leva a linha “quais são as alternativas para lançar exceções” e lista alguns problemas com cada uma das alternativas. Tendo feito isso, conclui que, como não podemos encontrar uma alternativa livre de problemas, devemos continuar lançando exceções.

O problema é que nenhum dos problemas que ele lista com as alternativas é tão ruim quanto o comportamento de exceção, que, vamos lembrar, é “comportamento indefinido do seu programa”. Algumas das objeções do autor incluem “esteticamente feio” e “encorajar o mau estilo”. Agora, o que você preferiria ter? Um programa com estilo ruim ou um que exibisse um comportamento indefinido?

P: Então, a minha pergunta é a seguinte: se jogar de um destrutor resulta em um comportamento indefinido, como você lida com erros que ocorrem durante um processo de destruição?

A: Existem várias opções:

  1. Deixe as exceções fluírem do seu destruidor, independentemente do que está acontecendo em outro lugar. E, ao fazê-lo, esteja ciente (ou mesmo com medo) de que std :: terminate pode seguir.

  2. Nunca deixe a exceção fluir do seu destruidor. Pode ser escrever em um log, algum texto ruim vermelho grande se você puder.

  3. my fave : Se std::uncaught_exception retornar false, deixe as exceções fluirem. Se retornar true, volte para a abordagem de log.

Mas é bom jogar nos dorsores?

Eu concordo com a maioria dos itens acima, de que jogar é melhor evitado no destruidor, onde pode ser. Mas às vezes é melhor aceitar que isso pode acontecer e lidar bem com isso. Eu escolheria 3 acima.

Existem alguns casos estranhos em que é realmente uma ótima idéia jogar de um destruidor. Como o código de erro “deve verificar”. Este é um tipo de valor que é retornado de uma function. Se o chamador ler / verificar o código de erro contido, o valor retornado será destruído silenciosamente. Mas , se o código de erro retornado não tiver sido lido no momento em que os valores de retorno saírem do escopo, ele lançará alguma exceção, de seu destruidor .

Atualmente, eu sigo a política (que muitos estão dizendo) que as classs não deveriam ativamente lançar exceções de seus destruidores, mas deveriam, ao invés disso, fornecer um método público de “fechamento” para executar a operação que poderia falhar …

… mas eu acredito que os destruidores de classs do tipo container, como um vetor, não devem mascarar exceções lançadas de classs que eles contêm. Neste caso, eu realmente uso um método “free / close” que se chama recursivamente. Sim, eu disse recursivamente. Existe um método para essa loucura. A propagação de exceção depende do fato de haver uma pilha: Se ocorrer uma única exceção, os dois destrutores restantes ainda serão executados e a exceção pendente será propagada assim que a rotina retornar, o que é ótimo. Se múltiplas exceções ocorrerem, então (dependendo do compilador), a primeira exceção será propagada ou o programa terminará, o que é correto. Se ocorrerem tantas exceções que a recursion transborda a pilha, então algo está seriamente errado, e alguém vai descobrir, o que também é bom. Pessoalmente, eu errei do lado dos erros explodindo ao invés de estar escondido, secreto e insidioso.

O ponto é que o contêiner permanece neutro, e cabe às classs contidas decidir se elas se comportam ou se comportam mal no que diz respeito a lançar exceções de seus destruidores.

Eu estou no grupo que considera que o padrão “scoped guard” lançado no destruidor é útil em muitas situações – particularmente para testes unitários. No entanto, esteja ciente de que, em C ++ 11, lançar um destrutor resulta em uma chamada para std::terminate pois os destruidores são implicitamente anotados com noexcept .

Andrzej Krzemieński tem um ótimo post sobre o tema dos destruidores que lançam:

Ele aponta que o C ++ 11 possui um mecanismo para replace o padrão noexcept para destruidores:

Em C ++ 11, um destruidor é implicitamente especificado como noexcept . Mesmo se você não adicionar nenhuma especificação e definir seu destruidor assim:

  class MyType { public: ~MyType() { throw Exception(); } // ... }; 

O compilador ainda irá adicionar uma especificação noexcept ao seu destruidor. E isso significa que no momento em que o seu destrutor gerar uma exceção, o std::terminate será chamado, mesmo se não houver uma situação de dupla exceção. Se você está realmente determinado a permitir que seus destruidores joguem, você terá que especificar isso explicitamente; você tem três opções:

  • Especifique explicitamente seu destrutor como noexcept(false) ,
  • Herda sua class de outra que já especifique seu destrutor como noexcept(false) .
  • Coloque um membro de dados não estático em sua class que já especifique seu destrutor como noexcept(false) .

Finalmente, se você decidir jogar o destruidor, você deve sempre estar ciente do risco de uma dupla exceção (jogando enquanto a pilha está sendo desfeita devido a uma exceção). Isso causaria uma chamada para std::terminate e raramente é o que você deseja. Para evitar esse comportamento, você pode simplesmente verificar se já existe uma exceção antes de lançar uma nova usando std::uncaught_exception() .

Definir um evento de alarme. Geralmente, os events de alarme são a melhor forma de notificar falhas durante a limpeza de objects

Ao contrário dos construtores, onde lançar exceções pode ser uma maneira útil de indicar que a criação de objects foi bem-sucedida, exceções não devem ser lançadas em destruidores.

O problema ocorre quando uma exceção é lançada de um destruidor durante o processo de desenrolamento da pilha. Se isso acontecer, o compilador é colocado em uma situação em que não sabe se deve continuar o processo de desenrolamento da pilha ou manipular a nova exceção. O resultado final é que seu programa será encerrado imediatamente.

Consequentemente, o melhor curso de ação é simplesmente abster-se de usar exceções em destrutores completamente. Escreva uma mensagem para um arquivo de log.

Martin Ba (acima) está no caminho certo – você arquitetou de maneira diferente a lógica RELEASE e COMMIT.

Para o lançamento:

Você deve comer algum erro. Você está liberando memory, fechando conexões, etc. Ninguém mais no sistema deve ver essas coisas novamente, e você está devolvendo resources para o sistema operacional. Se parecer que você precisa de tratamento de erros real aqui, é provável que seja uma consequência de falhas de design em seu modelo de object.

Para Commit:

É aqui que você deseja o mesmo tipo de objects wrapper RAII que coisas como std :: lock_guard estão fornecendo para mutexes. Com aqueles que você não coloca a lógica de commit no dtor AT ALL. Você tem uma API dedicada para isso, então os objects wrapper que RAII o confirmam em THEIR dtors e lidam com os erros lá. Lembre-se, você pode recuperar exceções CATCH em um destrutor muito bem; sua emissão é mortal. Isso também permite implementar política e tratamento de erros diferentes apenas criando um wrapper diferente (por exemplo, std :: unique_lock vs. std :: lock_guard) e garante que você não se esqueça de chamar a lógica de confirmação – que é a única metade do caminho justificativa decente para colocá-lo em um dtor em 1º lugar.

Então, minha pergunta é a seguinte: se jogar de um destrutor resulta em um comportamento indefinido, como você lida com erros que ocorrem durante um processo de destruição?

O principal problema é este: você não pode falhar em falhar . O que significa falhar em falhar, afinal? Se o commit de uma transação para um database falhar e falhar (falha ao reverter), o que acontece com a integridade de nossos dados?

Como os destruidores são invocados para caminhos normais e excepcionais (fail), eles próprios não podem falhar ou então estamos “falhando em falhar”.

Este é um problema conceitualmente difícil, mas muitas vezes a solução é apenas encontrar uma maneira de garantir que a falha não falhe. Por exemplo, um database pode gravar alterações antes de se comprometer com uma estrutura ou arquivo de dados externo. Se a transação falhar, a estrutura do arquivo / dados poderá ser descartada. Tudo o que é necessário garantir é que as alterações feitas a partir dessa estrutura / arquivo externo sejam uma transação atômica que não pode falhar.

A solução pragmática é, talvez, apenas garantir que as chances de falhar na falha sejam astronomicamente improváveis, já que tornar as coisas impossíveis de falhar pode ser quase impossível em alguns casos.

A solução mais apropriada para mim é escrever sua lógica de não limpeza de maneira que a lógica de limpeza não possa falhar. Por exemplo, se você for tentado a criar uma nova estrutura de dados para limpar uma estrutura de dados existente, talvez seja possível criar previamente essa estrutura auxiliar para que não seja mais necessário criá-la dentro de um destruidor.

Isso tudo é muito mais fácil dizer do que fazer, admitidamente, mas é a única maneira realmente correta que eu vejo para fazer isso. Às vezes, acho que deveria haver uma capacidade de escrever lógica de destruição separada para caminhos de execução normal, exceto por aqueles destruidores que se sentem um pouco como se tivessem o dobro de responsabilidades ao tentar lidar com ambos (um exemplo são proteções de escopo que exigem demissão explícita não exigiriam isso se pudessem diferenciar caminhos de destruição excepcionais dos não excepcionais).

Ainda assim, o problema final é que não podemos deixar de falhar, e é um problema de projeto conceitual difícil de resolver perfeitamente em todos os casos. Isso fica mais fácil se você não ficar muito envolvido em estruturas de controle complexas com toneladas de objects pequeninos interagindo uns com os outros e, em vez disso, modelando seus projetos de uma maneira um pouco mais volumosa (exemplo: sistema de partículas com um destruidor para destruir toda a partícula sistema, não um destruidor não-trivial separado por partícula). Quando você modela seus projetos nesse nível mais grosseiro, você tem menos destruidores não-triviais para lidar, e também pode, muitas vezes, pagar qualquer sobrecarga de memory / processamento necessária para garantir que seus destruidores não falhem.

E essa é uma das soluções mais fáceis, naturalmente, é usar destrutores com menos frequência. No exemplo da partícula acima, talvez ao destruir / remover uma partícula, algumas coisas deveriam ser feitas que poderiam falhar por qualquer motivo. Nesse caso, em vez de invocar tal lógica através do dtor da partícula, que poderia ser executado em um caminho excepcional, você poderia ter feito tudo pelo sistema de partículas quando remover uma partícula. A remoção de uma partícula pode sempre ser feita durante um caminho não excepcional. Se o sistema for destruído, talvez ele possa purgar todas as partículas e não se incomodar com a lógica de remoção de partículas que pode falhar, enquanto a lógica que pode falhar só é executada durante a execução normal do sistema de partículas quando estiver removendo uma ou mais partículas.

Muitas vezes existem soluções como essas que surgem se você evita lidar com muitos objects pequeninos com destruidores não-triviais. Onde você pode se enroscar em uma bagunça onde parece quase impossível ser exceção – segurança é quando você se enrosca em um monte de pequenos objects que todos têm ditadores não-triviais.

Ajudaria muito se nothrow / noexcept realmente fosse traduzido em um erro do compilador se qualquer coisa que o especificasse (incluindo funções virtuais que deveriam herdar a especificação noexcept de sua class base) tentasse invocar qualquer coisa que pudesse lançar. Dessa forma, poderíamos capturar tudo isso em tempo de compilation se realmente escrevêssemos um destruidor inadvertidamente, o que poderia acontecer.