A maioria das pessoas diz que nunca lança uma exceção a um destruidor - isso resulta em um comportamento indefinido. Stroustrup argumenta que "o destruidor de vetor chama explicitamente o destruidor para cada elemento. Isso implica que, se um destruidor de elemento é lançado, a destruição de vetor falha ... Não há realmente uma boa maneira de proteger contra exceções geradas por destruidores, portanto a biblioteca não garante se um destruidor de elemento joga "(do Apêndice E3.2) .
Este artigo parece dizer o contrário - que os destruidores de arremesso são mais ou menos válidos.
Portanto, minha pergunta é a seguinte: se jogar de um destruidor resulta em um comportamento indefinido, como você lida com erros que ocorrem durante um destruidor?
Se ocorrer um erro durante uma operação de limpeza, você simplesmente o ignora? Se é um erro que pode ser potencialmente manipulado na pilha, mas não está correto no destruidor, não faz sentido lançar uma exceção para fora do destruidor?
Obviamente, esses tipos de erros são raros, mas possíveis.
fonte
xyz()
e mantenha o destruidor limpo da lógica não RAII.commit()
método seja chamado.Respostas:
Lançar uma exceção de um destruidor é perigoso.
Se outra exceção já estiver propagando, o aplicativo será encerrado.
Isso basicamente se resume a:
Qualquer coisa perigosa (ou seja, que possa gerar uma exceção) deve ser feita por métodos públicos (não necessariamente diretamente). O usuário da sua classe pode potencialmente lidar com essas situações usando os métodos públicos e capturando possíveis exceções.
O destruidor finaliza o objeto chamando esses métodos (se o usuário não fez isso explicitamente), mas as exceções lançadas são capturadas e eliminadas (após a tentativa de corrigir o problema).
Portanto, 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 objeto não estiver preocupado (pois o objeto será destruído), o destruidor é deixado para cuidar dos negócios.
Um exemplo:
std :: fstream
O método close () pode gerar uma exceção potencialmente. O destruidor chama close () se o arquivo foi aberto, mas garante que nenhuma exceção seja propagada para fora do destruidor.
Portanto, se o usuário de um objeto de arquivo quiser fazer um tratamento especial para problemas associados ao fechamento do arquivo, eles chamarão manualmente close () e manipularão 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 que exceções deixem destruidores
fonte
Jogar fora de um destruidor pode resultar em uma falha, porque esse destruidor pode ser chamado como parte do "Desbobinamento da pilha". O desenrolamento de pilha é um procedimento que ocorre quando uma exceção é lançada. Neste procedimento, todos os objetos que foram enviados para a pilha desde a "tentativa" e até a exceção ser lançada serão finalizados -> seus destruidores serão chamados. E durante esse procedimento, não é permitido outro lançamento de exceção, porque não é possível lidar com duas exceções por vez, portanto, isso provocará uma chamada para abortar (), o programa falhará e o controle retornará ao sistema operacional.
fonte
throw
mas ainda não tendo encontrado umcatch
bloco para ele) nesse caso,std::terminate
(notabort
) é chamado em vez de gerar uma (nova) exceção (ou continuar com a pilha se desenrolando).Temos que nos diferenciar aqui, em vez de seguir cegamente os conselhos gerais para casos específicos .
Observe que o seguinte ignora a questão dos contêineres de objetos e o que fazer diante de vários d'tores de objetos dentro dos contêineres. (E isso pode ser ignorado parcialmente, pois alguns objetos não são adequados para serem colocados em um contêiner.)
Todo o problema se torna mais fácil de pensar quando dividimos as classes em dois tipos. Um dtor de classe pode ter duas responsabilidades diferentes:
Se encararmos a questão dessa maneira, acho que se pode argumentar que a semântica (R) nunca deve causar uma exceção de um dtor, pois existe a) nada que possamos fazer sobre isso eb) muitas operações de recursos livres não fornecer até verificação de erros, por exemplo .
void
free(void* p);
Objetos com a semântica (C), como um objeto de arquivo que precisa com sucesso nivelá-lo de dados ou uma conexão de banco de dados ( "scope guardada"), que faz a cometer no dtor são de um tipo diferente: Nós podemos fazer algo sobre o erro (em nível de aplicação) e realmente não devemos continuar como se nada tivesse acontecido.
Se seguirmos a rota RAII e permitirmos objetos que tenham semântica (C) em seus d'tors, acho que também precisaremos permitir o caso ímpar em que esses d'tors podem ser lançados. Conclui-se que você não deve colocar esses objetos em contêineres e também segue que o programa ainda poderá
terminate()
se um commit disparar enquanto outra exceção estiver ativa.Com relação ao tratamento de erros (semântica de Confirmação / Reversão) e exceções, há uma boa conversa de um Andrei Alexandrescu : Tratamento de erros no fluxo de controle C ++ / Declarative (realizado na NDC 2014 )
Nos detalhes, ele explica como a biblioteca Folly implementa um
UncaughtExceptionCounter
para suasScopeGuard
ferramentas.(Devo observar que outros também tiveram idéias semelhantes.)
Embora a palestra não se concentre em jogar de um d'tor, ela mostra uma ferramenta que pode ser usada hoje para se livrar dos problemas de quando jogar de um d'tor.
No
futuro, nãopodeser uma característica std para isso,ver N3614 ,e uma discussão sobre isso .Upd '17: O recurso C ++ 17 std para isso é
std::uncaught_exceptions
afakt. Vou citar rapidamente o artigo cppref:fonte
finally
.finally
é um dtor. É sempre chamado, não importa o quê. Para uma aproximação sintática de finalmente, consulte as várias implementações scope_guard. Atualmente, com o maquinário instalado (mesmo no padrão, é C ++ 14?) Para detectar se o dtor está autorizado a jogar, ele pode até ser totalmente seguro.finally
é inerentemente uma ferramenta para (C). Se você não vê o porquê: considere por que é legítimo lançar exceções umas sobre as outras emfinally
blocos e por que o mesmo não ocorre com destruidores. (Em certo sentido, é um controlo de dados vs . Coisa Destructors são dados para a libertação,finally
é para controlo de libertao Eles são diferentes, é lamentável que ++ laços C-los juntos..)A verdadeira questão a se perguntar sobre jogar 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 destruo um
Foo
objeto e oFoo
destruidor lança uma exceção, o que posso razoavelmente fazer com ele? Posso registrá-lo ou ignorá-lo. Isso é tudo. Não consigo "consertar", porque oFoo
objeto já se foi. Na melhor das hipóteses, registro a exceção e continuo como se nada tivesse acontecido (ou encerrado o programa). Isso realmente vale a pena causar um comportamento indefinido ao jogar de um destruidor?fonte
std::ofstream
o destruidor libera e fecha o arquivo. Pode ocorrer um erro de disco cheio durante a descarga, com o qual você pode absolutamente fazer algo útil: mostrar ao usuário uma caixa de diálogo de erro dizendo que o disco está sem espaço livre.É perigoso, mas também não faz sentido do ponto de vista de legibilidade / código.
O que você precisa perguntar é nesta situação
O que deve pegar a exceção? O chamador de foo deve? Ou deveria lidar com isso? Por que o chamador de foo deve se preocupar com algum objeto interno a foo? Pode haver uma maneira de a linguagem definir isso para fazer sentido, mas será ilegível e difícil de entender.
Mais importante, para onde vai a memória do Object? Para onde vai a memória que o objeto possuía? Ainda está alocado (ostensivamente porque o destruidor falhou)? Considere também que o objeto estava no espaço da pilha , portanto obviamente desapareceu.
Então considere este caso
Quando a exclusão do obj3 falha, como eu realmente excluo de uma maneira que garante que não falhe? É minha memória, caramba!
Agora considere no primeiro trecho de código que o Object desaparece automaticamente porque está na pilha enquanto o Object3 está no heap. Como o ponteiro para o Object3 se foi, você é um tipo de SOL. Você tem um vazamento de memória.
Agora, uma maneira segura de fazer as coisas é a seguinte
Veja também esta FAQ
fonte
int foo()
, você pode usar um bloco de função-tentativa-para envolver toda a função foo em um bloco de tentativa-captura, incluindo a captura de destruidores, se você quiser fazer isso. Ainda não é a abordagem preferida, mas é uma coisa.Do rascunho da ISO para C ++ (ISO / IEC JTC 1 / SC 22 N 4411)
Portanto, os destruidores geralmente devem capturar exceções e não deixá-los se propagar para fora do destruidor.
fonte
Seu destruidor pode estar executando dentro de uma cadeia de outros destruidores. Lançar uma exceção que não é capturada pelo chamador imediato pode deixar vários objetos em um estado inconsistente, causando ainda mais problemas e ignorando o erro na operação de limpeza.
fonte
Eu estou no grupo que considera que o padrão de "guarda de escopo" lançado no destruidor é útil em muitas situações - particularmente para testes de unidade. No entanto, lembre-se de que, no C ++ 11, lançar um destruidor resulta em uma chamada para,
std::terminate
pois os destruidores são implicitamente anotadosnoexcept
.Andrzej Krzemieński tem um ótimo post sobre o tema dos destruidores que lançam:
Ele ressalta que o C ++ 11 tem um mecanismo para substituir o padrão
noexcept
dos destruidores:Finalmente, se você decidir lançar o destruidor, você deve sempre estar ciente do risco de uma exceção dupla (lançada enquanto a pilha está sendo desenrolada devido a uma exceção). Isso causaria uma chamada
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 usandostd::uncaught_exception()
.fonte
Todo mundo já explicou por que jogar destruidores é terrível ... o que você pode fazer sobre isso? Se você estiver executando uma operação que pode falhar, crie um método público separado que execute a limpeza e possa gerar 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:
fonte
Como complemento às respostas principais, que são boas, abrangentes e precisas, eu gostaria de comentar sobre o artigo que você faz referência - aquele que diz "lançar exceções em destruidores não é tão ruim".
O artigo segue a linha "quais são as alternativas para gerar exceções" e lista alguns problemas com cada uma das alternativas. Feito isso, conclui que, como não conseguimos encontrar uma alternativa sem problemas, devemos continuar lançando exceções.
O problema é que nenhum dos problemas listados nas alternativas é tão ruim quanto o comportamento de exceção, que, lembre-se, é "comportamento indefinido do seu programa". Algumas das objeções do autor incluem "esteticamente feio" e "incentivar o mau estilo". Agora, o que você prefere? Um programa com um estilo ruim ou com comportamento indefinido?
fonte
A: Existem várias opções:
Deixe as exceções fluírem do seu destruidor, independentemente do que está acontecendo em outro lugar. E, ao fazer isso, esteja ciente (ou até com medo) de que std :: terminate pode seguir.
Nunca deixe a exceção fluir para fora do seu destruidor. Pode ser gravado em um log, se houver algum texto grande e vermelho incorreto.
meu favorito : Se
std::uncaught_exception
retornar falso, deixe que as exceções fluam. Se retornar true, volte para a abordagem de log.Mas é bom jogar d'tors?
Eu concordo com a maioria dos itens acima, que jogar é melhor evitar 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 para lançar de um destruidor. Como o código de erro "deve verificar". Este é um tipo de valor que é retornado de uma função. 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 retornados ficarem fora do escopo, ele lançará alguma exceção em seu destruidor .
fonte
Atualmente, sigo a política (que muitos estão dizendo) de que as classes não devem lançar ativamente exceções de seus destruidores, mas devem fornecer um método público "fechado" para executar a operação que pode falhar ...
... mas acredito que os destruidores de classes do tipo contêiner, como um vetor, não devem mascarar exceções lançadas das classes que contêm. Nesse 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 da existência de uma pilha: se uma única exceção ocorrer, os dois destruidores restantes ainda serão executados e a exceção pendente será propagada assim que a rotina retornar, o que é ótimo. Se ocorrerem várias exceções, (dependendo do compilador), a primeira exceção será propagada ou o programa será finalizado, o que é bom. Se ocorrerem tantas exceções que a recursão transborda a pilha, algo está seriamente errado, e alguém vai descobrir isso, o que também é bom. Pessoalmente,
O ponto é que o contêiner permanece neutro, e cabe às classes contidas decidir se elas se comportam ou se comportam mal com relação ao lançamento de exceções de seus destruidores.
fonte
Diferentemente dos construtores, em que o lançamento de exceções pode ser uma maneira útil de indicar que a criação do objeto foi bem-sucedida, as exceções não devem ser lançadas nos 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.
Conseqüentemente, o melhor curso de ação é apenas abster-se de usar exceções em destruidores por completo. Escreva uma mensagem em um arquivo de log.
fonte
Martin Ba (acima) está no caminho certo - você arquiteta de maneira diferente para a lógica RELEASE e COMMIT.
Para Liberação:
Você deve comer algum erro. Você está liberando memória, fechando conexões etc. Ninguém mais no sistema deve VER essas coisas novamente e está devolvendo recursos ao sistema operacional. Se parece que você precisa de um tratamento real de erros aqui, provavelmente é uma consequência de falhas de design no seu modelo de objeto.
Para Confirmar:
É aqui que você deseja o mesmo tipo de objeto wrapper RAII que coisas como std :: lock_guard estão fornecendo para mutexes. Com aqueles que você não coloca a lógica de confirmação no dtor AT ALL. Você tem uma API dedicada para isso e, em seguida, empacota os objetos que a RAII confirmarão nos seus dtors e manipularão os erros lá. Lembre-se, você pode capturar exceções em um destruidor muito bem; é a emissão deles que é mortal. Isso também permite implementar políticas e tratamento de erros diferentes, criando um wrapper diferente (por exemplo, std :: unique_lock vs. std :: lock_guard) e garante que você não esqueça de chamar a lógica de confirmação - que é a única maneira justificativa decente para colocá-lo em um dtor em primeiro lugar.
fonte
O principal problema é este: você não pode falhar . O que significa deixar de falhar, afinal? Se a confirmação de uma transação com um banco de dados falha, e falha (falha na reversão), o que acontece com a integridade de nossos dados?
Como os destruidores são invocados para caminhos normais e excepcionais (falha), eles mesmos não podem falhar ou então estamos "falhando na falha".
Esse é um problema conceitualmente difícil, mas geralmente a solução é encontrar uma maneira de garantir que a falha não possa falhar. Por exemplo, um banco de dados pode gravar alterações antes de se comprometer com uma estrutura ou arquivo de dados externo. Se a transação falhar, a estrutura de arquivo / dados poderá ser descartada. Tudo o que é necessário garantir é confirmar as alterações dessa estrutura / arquivo externo em uma transação atômica que não pode falhar.
A solução mais adequada para mim é escrever sua lógica de não limpeza de uma maneira que a lógica de limpeza não possa falhar. Por exemplo, se você estiver tentado a criar uma nova estrutura de dados para limpar uma estrutura de dados existente, talvez seja melhor criar essa estrutura auxiliar com antecedência, para que não seja mais necessário criá-la dentro de um destruidor.
Tudo isso é muito mais fácil dizer do que fazer, é certo, mas é a única maneira realmente adequada de ver isso. Às vezes, acho que deveria haver a capacidade de escrever lógicas destruidoras separadas para caminhos normais de execução, além das excepcionais, já que às vezes os destruidores sentem um pouco como se tivessem o dobro de responsabilidades tentando lidar com ambos (um exemplo são os guardas de escopo que exigem dispensa explícita ; eles 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 falhar, e é um difícil problema de projeto conceitual resolver perfeitamente em todos os casos. Fica mais fácil se você não se envolver demais em estruturas de controle complexas com toneladas de objetos pequenininhos interagindo entre si e modelar seus projetos de 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). Ao modelar seus projetos nesse tipo de nível mais grosseiro, você tem menos destruidores não triviais para lidar e também pode pagar com qualquer sobrecarga de memória / processamento necessária para garantir que seus destruidores não falhem.
E essa é uma das soluções mais fáceis, naturalmente, é usar destruidores com menos frequência. No exemplo de partícula acima, talvez ao destruir / remover uma partícula, algumas coisas devam ser feitas que possam falhar por qualquer motivo. Nesse caso, em vez de invocar essa lógica através do dtor da partícula que poderia ser executado em um caminho excepcional, você poderia fazer tudo pelo sistema de partículas quando remover uma partícula. A remoção de uma partícula sempre pode ser feita durante um caminho não excepcional. Se o sistema for destruído, talvez ele possa limpar todas as partículas e não se incomodar com a lógica de remoção de partículas individual que pode falhar, enquanto a lógica que pode falhar é executada apenas durante a execução normal do sistema de partículas ao remover uma ou mais partículas.
Muitas vezes existem soluções como essa que surgem se você evitar lidar com muitos objetos pequenininhos com destruidores não triviais. Onde você pode se envolver em uma confusão onde parece quase impossível ser exceção - a segurança é quando você se envolve em muitos objetos pequenininhos, todos com doutores não triviais.
Ajudaria muito se o nothrow / noexcept realmente fosse traduzido em um erro do compilador se algo que o especificasse (incluindo funções virtuais que deveriam herdar a especificação noexcept de sua classe base) tentasse invocar qualquer coisa que pudesse gerar. Dessa forma, seríamos capazes de capturar todas essas coisas em tempo de compilação, se realmente escrevermos um destruidor inadvertidamente que poderia ser lançado.
fonte
Defina um evento de alarme. Normalmente, os eventos de alarme são a melhor forma de notificar falha ao limpar objetos
fonte