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.