Pergunta

A maioria das pessoas dizem não lançar uma exceção fora de um destruidor - isso resulta em um comportamento indefinido. Stroustrup faz o ponto que "o destruidor vector invoca explicitamente o destrutor para cada elemento. Isto implica que, se um elemento destruidor lança, a destruição vector falhar ... Não há realmente nenhuma boa maneira de proteger contra as exceções lançadas a partir de destruidores, de modo a biblioteca não garante se um elemento destruidor lança"(do Apêndice E3.2) .

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

Então, minha pergunta é esta -? Se jogando em 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 ignorá-lo? Se é um erro que pode, potencialmente, ser tratada a pilha mas não à direita no destruidor, não faz sentido para lançar uma exceção fora do destruidor?

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

Foi útil?

Solução

lançar uma exceção fora de um processo de destruição é perigoso.
Se outra exceção já está propagando a aplicação será terminada.

#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 poderia lançar uma exceção) deve ser feito através de métodos públicos (não necessariamente diretamente). O usuário de sua classe pode, então, potencialmente lidar com essas situações usando os métodos públicos e captura todas as exceções possíveis.

O processo de destruição, então, acabar com o objeto chamando estes métodos (se o usuário não fazê-lo explicitamente), mas quaisquer excepções jogue são capturados e caiu (depois de tentar resolver o problema).

Então, na verdade você passar a responsabilidade para o usuário. Se o usuário estiver em uma posição para exceções corretas eles vão chamar manualmente as funções e processos adequados quaisquer erros. Se o usuário do objeto não está preocupado (como o objeto será destruído), em seguida, o destruidor é deixado para cuidar dos negócios.

Um exemplo:

std :: fstream

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

Assim, se o usuário de um objeto de arquivo quer fazer um tratamento especial para os problemas associados ao encerramento do processo eles vão chamar manualmente close () e lidar com quaisquer excepções. Se, por outro lado, eles não se importam em seguida, 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 no "mais eficaz C ++"
Item 11: exceções Impedir deixando destruidores

Outras dicas

Jogando fora de um processo de destruição pode resultar em um acidente, porque este processo de destruição pode ser chamado como parte de "Pilha desenrolar". Pilha desenrolar é um procedimento que ocorre quando uma exceção é lançada. Neste procedimento, todos os objetos que foram empurrados para a pilha uma vez que a "tentativa" e até que a exceção foi lançada, será encerrada -> seus destruidores serão chamados. E durante este procedimento, outro lance de exceção não é permitido, porque não é possível lidar com duas exceções de cada vez, portanto, isso vai provocar uma chamada para abort (), o programa irá falhar eo controle retornará para o OS.

Temos que diferenciada aqui em vez de seguir cegamente Geral conselhos para específica casos.

Note que a seguinte ignora a questão de embalagens de objetos e o que fazer em face de várias d'res de objetos dentro de recipientes. (E isso pode ser ignorado parcialmente, já que alguns objetos são apenas nenhuma boa opção para colocar em um recipiente.)

Todo o problema se torna mais fácil para se pensar quando nos separamos aulas em dois tipos. A dtor classe pode ter duas responsabilidades diferentes:

  • (R) semântica libertação (aka libertar essa memória)
  • (C) commit semântica (aka flush arquivo no disco)

Se vemos a questão dessa maneira, então eu acho que pode-se argumentar que (R) semântica nunca deve causar uma exceção de um dtor que haja a) nada que possamos fazer sobre isso e b) muitos sem recurso operações nem sequer prever a verificação de erros, por exemplo, void free(void* p);.

Objetos com (C) semântica, como um objeto de arquivo que precisa com sucesso nivelá-lo de dados ou um ( "scope guardada") conexão com o banco que faz um cometer no dtor são de um tipo diferente: Nós pode fazer algo sobre o erro (no nível de aplicação) e nós realmente não deve continuar como se nada tivesse acontecido.

Se seguirmos a rota RAII e permitir a objetos que têm (C) semântica em seus d'res eu acho que, em seguida, temos também para permitir o caso estranho onde tais d'res pode jogar. Segue-se que você não deve colocar esses objetos em recipientes e segue-se também que o programa pode ainda terminate() se um commit-dtor joga enquanto outra exceção é ativo.


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

Nos detalhes, ele explica como os implementos Folly biblioteca um UncaughtExceptionCounter por sua ScopeGuard ferramentas.

(Devo observar que outros também tinham idéias semelhantes.)

Enquanto a conversa não se concentrar em jogar a partir de um d'tor, ele mostra uma ferramenta que pode ser usado hoje para se livrar do problemas com quando jogar de um d'tor.

No futuro , não pode ser uma característica std para isso, veja N3614 , e discussão sobre isso .

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

Notas

Um exemplo onde é usado int-retornando uncaught_exceptions é ... ... primeiro cria um objeto de guarda e registra o número de exceções.Ele em seu construtor. A saída é realizada pela Guarda objeto de destructor menos que foo () lança ( no caso do número de uncaught que exceções no processo de destruição é maior do que o que o construtor observados )

A verdadeira questão a se perguntar sobre jogar em um destrutor é "O que pode o chamador ver com isso?" Existe realmente alguma coisa útil que você pode fazer com a exceção, que iria compensar os perigos criados por atirar em um destrutor?

Se eu destruir um objeto Foo, e os lançamentos Foo destructor uma exceção, o que posso razoavelmente fazer com ele? Eu posso registrá-lo, ou eu posso ignorá-lo. Isso é tudo. Eu não pode "consertar", porque o objeto Foo já se foi. Melhor caso, eu registrar a exceção e continuar como se nada tivesse acontecido (ou terminar o programa). É que realmente vale potencialmente causando um comportamento indefinido, jogando em um destrutor?

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

O que você tem a fazer é nesta situação

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

O que deve capturar a exceção? Se o chamador de foo? Ou deve foo lidar com isso? Por que o chamador de cuidados foo sobre algum objeto interno para foo? Pode haver uma maneira a linguagem define este sentido de fazer, mas que vai ser ilegível e difícil de entender.

Mais importante, onde é que a memória para ir objeto? Onde é que a memória do objeto de propriedade ir? Ainda é alocado (aparentemente porque o destruidor não)? Considere também o objeto estava em espaço de pilha , pelo que a sua, obviamente, ido de qualquer maneira.

Em seguida, considere neste 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 falhar, como eu realmente eliminar de uma forma que seja garantida a não falhar? Sua minha droga memória!

Agora, considere em primeiro objeto trecho de código vai embora automaticamente porque o seu na pilha enquanto object3 é na pilha. Desde o ponteiro para object3 se foi, você é do tipo SOL. Você tem um vazamento de memória.

Agora, uma forma segura de fazer as coisas é o seguinte

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

};

Veja também este FAQ

A partir do projecto de norma ISO para C ++ (ISO / IEC JTC 1 / SC 22 N 4411)

Então destruidores geralmente deve capturar exceções e não deixá-los se propagam para fora do destruidor.

3 O processo de chamar destruidores para objetos automáticos construídos no caminho de um bloco try a um throw- expressão é chamado de “desenrolamento de pilha.” [Nota: Se um destruidor chamado durante pilha desenrolar sai com um excepção, std :: encerrar é chamado (15.5.1). Então destruidores geralmente deve capturar exceções e não deixar eles se propagam para fora do destruidor. - nota final]

Seu destructor pode estar executando dentro de uma cadeia de outros destruidores. Lançar uma exceção que não é capturada pelo seu chamador imediato pode deixar vários objetos em um estado inconsistente, causando ainda mais problemas, em seguida, ignorando o erro na operação de limpeza.

Todo mundo tem explicou por destruidores de arremesso 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 executa a limpeza e pode lançar exceções arbitrárias. Na maioria dos casos, os usuários irão ignorar isso. Se os usuários querem monitorar o sucesso / insucesso 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...
};

Como um complemento para as principais respostas, que são bons, abrangente e precisa, eu gostaria de comentar sobre o artigo você faz referência -. Aquele que diz "lançar exceções em destruidores não é tão ruim"

O artigo toma a linha "quais são as alternativas para lançar exceções", e listas de alguns problemas com cada uma das alternativas. Tendo feito isso, conclui que, porque não podemos encontrar uma alternativa livre de problemas devemos continuar jogando exceções.

O problema é que é que nenhum dos problemas que listas com as alternativas são nem de longe tão ruim quanto o comportamento de exceção, que, lembremos, é "um comportamento indefinido de seu programa". Algumas das objeções do autor incluem "esteticamente feio" e "incentivar um estilo ruim". Agora que você preferiria ter? Um programa com mau estilo, ou um que exibiram comportamento indefinido?

Eu estou no grupo que considera que o "guarda de escopo" padrão de arremesso no processo de destruição é útil em muitas situações - especialmente para testes de unidade. No entanto, estar ciente de que em C ++ 11, jogando em um processo de destruição resulta em uma chamada para std::terminate desde destruidores são implicitamente anotado com noexcept.

Andrzej Krzemienski tem um ótimo post sobre o tema da destruidores que jogar:

Ele ressalta que C ++ 11 tem um mecanismo para substituir o noexcept padrão para destruidores:

Em C ++ 11, um destruidor é especificada implicitamente como noexcept. Mesmo se você adicionar nenhuma especificação e definir o seu processo de destruição como este:

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

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

  • Especificar explicitamente o seu destruidor como noexcept(false),
  • Herdar sua classe a partir de um outro que já especifica seu destruidor como noexcept(false).
  • Coloque um membro de dados não-estático em sua classe que já especifica sua destructor como noexcept(false).

Finalmente, se você decidir jogar no processo de destruição, você deve sempre estar ciente do risco de um duplo exceção (jogando enquanto a pilha está sendo descontrair por causa de uma exceção). Isso faria com que uma chamada para std::terminate e raramente é o que você quer. Para evitar esse comportamento, você pode simplesmente verificar se já existe uma exceção antes de lançar um novo usando std::uncaught_exception().

Q: Então, minha pergunta é esta - se jogando de um destruidor resultados em 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 fluir para fora do seu destruidor, independentemente do que está acontecendo em outros lugares. E ao fazê-lo estar ciente (ou mesmo com medo) que std :: terminar pode seguir.

  2. Nunca deixe fluir exceção do seu destruidor. Pode ser escrita para um log, algum grande texto vermelho ruim se você pode.

  3. meu fave : Se std::uncaught_exception retornos falsos, permitem exceções fluir para fora. Se ele retorna true, em seguida, cair de volta para a abordagem de registro.

Mas é bom para jogar d'res?

Eu concordo com a maioria dos acima que jogando é melhor evitar em destructor, onde ele pode ser. Mas às vezes você é melhor fora de aceitar isso pode acontecer, e segurá-lo bem. Eu escolheria 3 acima.

Existem alguns casos estranhos onde sua realmente um ótima idéia para jogar em um destrutor. Como o código de erro "deve verificar". Este é um tipo de valor que é retornado de uma função. Se o chamador lê cheques / o código de erro contido, o valor retornado destrói silenciosamente. e , se o código de erro retornado não foi lido pelo tempo os valores de retorno sai do escopo, ele vai lançar alguma exceção, de seu destruidor .

Eu atualmente seguem a política (que muitos estão dizendo) que as classes não deve jogar ativamente exceções de seus destruidores, mas deve sim fornecer um público "fechar" método para executar a operação que poderia falhar ...

... mas eu acredito destruidores para as classes de tipo recipiente, como um vetor, não deve mascarar exceções lançadas a partir de classes que contêm. Neste caso, eu realmente usar um método "livre / close" que se chama de forma recursiva. Sim, eu disse de forma recursiva. Há um método a esta loucura. propagação de exceção depende da existência de uma pilha: Se ocorrer uma única exceção, em seguida, ambos os destruidores restantes continuarão a ser executados ea exceção pendente irá propagar uma vez que os retornos de rotina, que é grande. Se ocorrerem vários exceções, então (dependendo do compilador) quer que a primeira exceção irá propagar ou o programa será encerrado, o que está bem. Se ocorrer tantas exceções que a recursividade transborda a pilha então algo está seriamente errado, e alguém vai descobrir mais sobre ele, que também está bem. Pessoalmente, eu errar do lado de erros explodir em vez de ser escondido, secreto e insidioso.

O ponto é que o recipiente permanece neutro, e é até as classes contidas para decidir se eles se comportam ou misbehave em relação ao lançar exceções de seus destruidores.

Martin Ba (acima) é sobre o Track-direito você arquiteto de forma diferente para a liberação e COMMIT lógica.

Para Lançamento:

Você deve comer todos os erros. Você está liberando memória, fechando conexões, etc. Ninguém mais no sistema deve sempre ver essas coisas de novo, e você está recursos devolver para o sistema operacional. Se parece que você precisa verdadeiro erro manipulação aqui, é provavelmente uma consequência de falhas de projeto em seu modelo de objeto.

Para Commit:

Este é o local onde você deseja que o mesmo tipo de RAII invólucro objetos que coisas como std :: lock_guard estão fornecendo para mutexes. Com aqueles que você não colocar a lógica cometer no dtor AT ALL. Você tem uma API específica para isso, os objetos depois de invólucro que vai RAII cometê-lo em suas dtors e lidar com os erros de lá. Lembre-se, você pode capturar exceções em um destruidor muito bem; sua emissão lhes que é mortal. Isso também permite que você implementar a política e diferente de tratamento de erro apenas pela construção de um invólucro diferente (por exemplo, std :: unique_lock vs std :: lock_guard), e garante que você não vai esquecer de chamar a cometer logic- que é o único meio caminho justificação decente para colocá-lo em um dtor no 1º lugar.

Definir um evento de alarme. Normalmente eventos de alarme são melhores forma de notificar a falha durante a limpeza de objetos

Ao contrário construtores, onde exceções jogando pode ser uma maneira útil para indicar que a criação do objeto conseguiu, exceções não devem ser jogados em destruidores.

O problema ocorre quando uma exceção é lançada a partir de um processo de destruição durante o processo de desenrolamento de pilha. Se isso acontecer, o compilador é colocado em uma situação onde ele não sabe se quer continuar o processo de desenrolamento de pilha ou lidar com a nova exceção. O resultado final é que seu programa será encerrada imediatamente.

Por isso, o melhor curso de ação é apenas para se abster de usar exceções em destruidores completamente. Escrever para um arquivo de log em seu lugar.

Assim, a minha pergunta é esta - se jogando de um resultado destructor em comportamento indefinido, como você lida com os erros que ocorrem durante uma destructor?

O principal problema é o seguinte: você não pode não falham . O que significa a falhar a falhar, depois de tudo? Se cometer uma transação para um banco de dados falhar, e ele não consegue falhar (não rollback), o que acontece com a integridade de nossos dados?

Desde destruidores são invocados para ambos (falha) caminhos normais e excepcionais, eles próprios não pode falhar ou então estamos "não falhar".

Este é um problema conceitualmente difícil, mas muitas vezes a solução é apenas para encontrar uma maneira de se certificar de que não não pode falhar. Por exemplo, um banco de dados pode escrever as alterações antes de cometer a uma estrutura de dados externo ou arquivo. Se a transação falhar, então a estrutura de arquivos / dados podem ser jogados fora. Tudo o que tem de garantir, em seguida, é que cometer as mudanças de que a estrutura externa / arquivar uma transação atômica que não pode falhar.

A solução pragmática é, talvez, apenas certifique-se de que as chances de falhando em caso de falha são astronomicamente improvável, uma vez que fazer as coisas impossível deixar de falhar pode ser quase impossível em alguns casos.

A solução mais adequada para mim é escrever sua lógica não-limpeza de uma forma tal que a lógica de limpeza não pode falhar. Por exemplo, se você está tentado a criar uma nova estrutura de dados, a fim de limpar uma estrutura de dados existente, então talvez você poderia tentar criar essa estrutura auxiliar com antecedência para que nós já não temos que criá-lo dentro de um destruidor.

Isso tudo é muito mais fácil dizer do que fazer, é certo, mas é a única maneira realmente adequada vejo ir sobre ele. Às vezes eu acho que deveria haver uma capacidade de escrever lógica destructor separado para caminhos de execução normais longe uns excepcionais, já que às vezes destruidores sentir um pouco como eles têm o dobro das responsabilidades, tentando lidar com ambos (um exemplo é guardas de escopo que exigem a demissão explícita ;. não exigiria isso se pudessem diferenciar caminhos de destruição excepcionais dos não-excepcionais)

Ainda assim, o problema principal é que não podemos deixar de falhar, e é um problema de design conceitual difícil de resolver perfeitamente em todos os casos. Ele faz ficar mais fácil se você não ficar muito embrulhado em estruturas de controle complexos com toneladas de objetos pequeninos que interagem uns com os outros, e, em vez modelar seus projetos de forma ligeiramente mais volumoso (exemplo: sistema de partículas com um destruidor para destruir toda a partícula sistema, não um processo de destruição de não-trivial separado por partícula). Quando você modelar seus desenhos este tipo de nível mais grosseiro, você tem destruidores menos não triviais para lidar com, e também muitas vezes pode pagar o que quer que a memória / sobrecarga de processamento é necessário para se certificar de seus destruidores não pode falhar.

E isso é uma das soluções mais fáceis naturalmente é destruidores de uso com menos frequência. No exemplo de partículas acima, talvez a destruir / remoção de uma partícula, algumas coisas devem ser feitas que poderiam falhar por qualquer motivo. Nesse caso, em vez de chamar essa lógica através dtor da partícula que pode ser executado em um caminho excepcional, você poderia sim ter tudo feito pelo sistema de partículas quando se remove uma partícula. A remoção de uma partícula pode ser sempre feito durante um caminho não excepcional. Se o sistema for destruída, talvez ele pode simplesmente remover todas as partículas e não se preocupar com essa lógica de remoção de partículas individual que pode falhar, enquanto a lógica que pode deixar só é executado durante a execução normal do sistema de partículas quando ele está removendo uma ou mais partículas.

Existem muitas vezes soluções como o que surgir se você evitar lidar com lotes de objetos pequeninos com destruidores não-triviais. Onde você pode perder-se em uma confusão onde parece quase impossível de ser exceção à segurançaé quando você perder-se em lotes de objetos pequeninos que todos têm dtors não-triviais.

Ajudaria muito se nothrow / noexcept realmente traduzido em um erro do compilador se alguma coisa que especifica que (incluindo funções virtuais que devem herdar a especificação noexcept de sua classe base) tentou invocar qualquer coisa que poderia jogar. Desta forma, seria capaz de pegar tudo isso em tempo de compilação se nós realmente escrever um destrutor, inadvertidamente, que poderia jogar.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top