jogando exceções fora de um destruidor

257

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.

Greg Rogers
fonte
36
"Duas exceções ao mesmo tempo" é uma resposta das ações, mas não é a verdadeira razão. O verdadeiro motivo é que uma exceção deve ser lançada se, e somente se, as pós-condições de uma função não puderem ser atendidas. A pós-condição de um destruidor é que o objeto não existe mais. Isso não pode acontecer. Qualquer operação de fim de vida útil propensa a falhas deve, portanto, ser chamada como um método separado antes que o objeto fique fora do escopo (funções sensíveis normalmente normalmente têm apenas um caminho de sucesso).
spraff
29
@spraff: Você está ciente de que o que você disse implica "jogar fora o RAII"?
Kos
16
@spraff: ter que chamar "um método separado antes que o objeto saia do escopo" (como você escreveu) realmente joga fora o RAII! O código que usa esses objetos terá que garantir que esse método seja chamado antes que o destruidor seja chamado. Finalmente, essa ideia não ajuda em nada.
Frunsi 8/08/12
8
@ Frunsi não, porque esse problema decorre do fato de o destruidor estar tentando fazer algo além da mera liberação de recursos. É tentador dizer "eu sempre quero fazer XYZ" e pensar que esse é um argumento para colocar essa lógica no destruidor. Não, não seja preguiçoso, escreva xyz()e mantenha o destruidor limpo da lógica não RAII.
spraff 11/08/2012
6
@Frunsi Por exemplo, confirmar algo para arquivar não é necessariamente bom no destruidor de uma classe que representa uma transação. Se a confirmação falhar, é tarde demais para lidar com isso quando todo o código envolvido na transação estiver fora do escopo. O destruidor deve descartar a transação, a menos que um commit()método seja chamado.
Nicholas Wilson

Respostas:

198

Lançar uma exceção de um destruidor é perigoso.
Se outra exceção já estiver propagando, o aplicativo será encerrado.

#include <iostream>

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 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

Martin York
fonte
5
"A menos que você não se importe de potencialmente encerrar o aplicativo, provavelmente deve engolir o erro." - provavelmente deve ser a exceção (perdoe o trocadilho) e não a regra - ou seja, falhar rapidamente.
Erik Forbes
15
Discordo. O encerramento do programa interrompe o desenrolar da pilha. Não será mais chamado destruidor. Quaisquer recursos abertos serão deixados em aberto. Eu acho que engolir a exceção seria a opção preferida.
Martin York
20
O sistema operacional pode limpar os recursos e é o proprietário desligado. Memória, FileHandles etc. Que tal recursos complexos: conexões com o banco de dados. Essa ligação ao ISS que você abriu (enviará automaticamente as conexões próximas)? Tenho certeza que a NASA deseja que você feche a conexão de maneira limpa!
Martin Iorque
7
Se um aplicativo "falhará rápido" ao abortar, ele não deve gerar exceções em primeiro lugar. Se houver uma falha ao passar o controle de volta à pilha, isso não deve ocorrer de uma maneira que possa causar a interrupção do programa. Um ou outro, não escolha os dois.
13742 Tom
2
@LokiAstari O protocolo de transporte que você está usando para se comunicar com uma espaçonave não consegue lidar com uma conexão interrompida? Ok ...
doug65536
54

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.

Gal Goldman
fonte
1
você pode por favor explicar como abort () foi chamado na situação acima. Significa que o controle da execução ainda estava com o compilador C ++
Krishna Oza
1
@Krishna_Oza: Muito simples: sempre que um erro é gerado, o código que gera um erro verifica um pouco que indica que o sistema de tempo de execução está no processo de desenrolamento de pilhas (ou seja, manipulando outros, throwmas ainda não tendo encontrado um catchbloco para ele) nesse caso, std::terminate(not abort) é chamado em vez de gerar uma (nova) exceção (ou continuar com a pilha se desenrolando).
Marc van Leeuwen 06/06
53

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:

  • (R) liberar semântica (também conhecida como liberar essa memória)
  • (C) confirmar a semântica (também conhecida como arquivo nivelado no disco)

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 UncaughtExceptionCounterpara suas ScopeGuardferramentas.

(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ão pode ser 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_exceptionsafakt. Vou citar rapidamente o artigo cppref:

Notas

Um exemplo em que int-returning uncaught_exceptionsé usado é ... ... primeiro cria um objeto de proteção e registra o número de exceções não capturadas em seu construtor. A saída é executada pelo destruidor do objeto de guarda, a menos que foo () ative ( nesse caso, o número de exceções não capturadas no destruidor é maior do que o que o construtor observou )

Martin Ba
fonte
6
Concordo plenamente. E adicionando mais uma semântica de reversão semântica (Ro). Utilizado normalmente na proteção de escopo. Como o caso do meu projeto, onde defini uma macro ON_SCOPE_EXIT. O caso da semântica de reversão é que qualquer coisa significativa poderia acontecer aqui. Portanto, não devemos realmente ignorar o fracasso.
precisa saber é o seguinte
Sinto que a única razão pela qual confirmamos a semântica nos destruidores é que o C ++ não suporta finally.
user541686
@ Mehrdad: 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.
Martin Ba
1
@ MartinBa: Eu acho que você perdeu o ponto do meu comentário, o que é surpreendente, pois eu estava de acordo com a sua noção de que (R) e (C) são diferentes. Eu estava tentando dizer que um dtor é inerentemente uma ferramenta para (R) e 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 em finallyblocos 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..)
user541686
1
@ Mehrdad: Ficando muito tempo aqui. Se desejar, você pode criar seus argumentos aqui: programmers.stackexchange.com/questions/304067/… . Obrigado.
Martin Ba
21

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 Fooobjeto e o Foodestruidor 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 o Fooobjeto 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?

Derek Park
fonte
11
Só notei ... jogar de um dtor nunca é um comportamento indefinido. Claro, pode chamar terminate (), mas esse é um comportamento muito bem especificado.
Martin Ba
4
std::ofstreamo 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.
Andy
13

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

O que você precisa perguntar é nesta 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 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

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 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

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

};

Veja também esta FAQ

Doug T.
fonte
Ressuscitando esta resposta, re: o primeiro exemplo, sobre 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.
tyree731
13

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.

3 O processo de chamar destruidores de objetos automáticos construídos no caminho de um bloco de tentativa a uma expressão de lançamento é chamado de "desenrolamento de pilha". [Nota: Se um destruidor chamado durante o desenrolamento da pilha sair com uma exceção, std :: terminate será chamado (15.5.1). Portanto, os destruidores geralmente devem capturar exceções e não deixá-los se propagar para fora do destruidor. - nota final]

Lothar
fonte
1
Não respondeu à pergunta - o OP já está ciente disso.
Arafangion
2
@Arafangion Duvido que ele estivesse ciente disso (std :: terminate sendo chamado), pois a resposta aceita fez exatamente o mesmo ponto.
Lothar
@Arafangion como em algumas respostas aqui algumas pessoas mencionaram que abort () está sendo chamado; Ou é que o std :: terminate, por sua vez, chama a função abort ().
Krishna Oza
7

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.

Franci Penov
fonte
7

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::terminatepois os destruidores são implicitamente anotados noexcept.

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 noexceptdos destruidores:

No C ++ 11, um destruidor é especificado implicitamente 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 adicionará especificações invisíveis noexceptao seu destruidor. E isso significa que, no momento em que seu destruidor lançar uma exceção, std::terminateserá chamado, mesmo se não houver uma situação de dupla exceção. Se você está realmente determinado a permitir que seus destruidores atinjam, terá que especificar isso explicitamente; você tem três opções:

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

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::terminatee 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().

GaspardP
fonte
6

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:

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 (e.g. file is open by another process)
    // the rest of the class omitted...
};
Tom
fonte
Estou procurando uma solução, mas eles estão tentando explicar o que aconteceu e por quê. Só quero deixar claro que a função fechar é chamada dentro do destruidor?
Jason Liu
5

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?

DJClayworth
fonte
1
Não comportamento indefinido, mas rescisão imediata.
Marc van Leeuwen
O padrão diz 'comportamento indefinido'. Esse comportamento é frequentemente rescisão, mas nem sempre.
precisa
Não, leia [exceto.terminar] em Tratamento de exceção -> Funções especiais (que é 15.5.1 na minha cópia do padrão, mas sua numeração provavelmente está desatualizada).
Marc van Leeuwen
2

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

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 fazer isso, esteja ciente (ou até com medo) de que std :: terminate pode seguir.

  2. 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.

  3. meu favorito : Se std::uncaught_exceptionretornar 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 .

MartinP
fonte
4
Seu favorito é algo que tentei recentemente, e você não deve fazê-lo. gotw.ca/gotw/047.htm
GManNickG
1

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.

Mateus
fonte
1

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.

Devesh Agrawal
fonte
1
Escrever uma mensagem no arquivo de log pode causar uma exceção.
21919 Konard Konard
1

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.

user3726672
fonte
1

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?

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 pragmática talvez seja apenas garantir que as chances de fracassar sejam astronomicamente improváveis, pois tornar as coisas impossíveis de deixar de falhar pode ser quase impossível em alguns casos.

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.

Dragon Energy
fonte
1
Destruição é falha agora?
curiousguy
Eu acho que ele quer dizer que os destruidores são chamados durante uma falha, para limpar essa falha. Portanto, se um destruidor for chamado durante uma exceção ativa, ele falhará na limpeza de uma falha anterior.
user2445507
0

Defina um evento de alarme. Normalmente, os eventos de alarme são a melhor forma de notificar falha ao limpar objetos

MRN
fonte