É `catch (…) {throw; } `uma má prática?

74

Embora eu concorde que pegar ... sem reler é realmente errado, no entanto, acredito que o uso de construções como esta:

try
{
  // Stuff
}
catch (...)
{
  // Some cleanup
  throw;
}

É aceitável nos casos em que o RAII não é aplicável . (Por favor, não pergunte ... nem todo mundo na minha empresa gosta de programação orientada a objetos e o RAII é frequentemente visto como "material escolar inútil" ...)

Meus colegas de trabalho dizem que você sempre deve saber quais exceções devem ser lançadas e que sempre pode usar construções como:

try
{
  // Stuff
}
catch (exception_type1&)
{
  // Some cleanup
  throw;
}
catch (exception_type2&)
{
  // Some cleanup
  throw;
}
catch (exception_type3&)
{
  // Some cleanup
  throw;
}

Existe uma boa prática bem admitida em relação a essas situações?

ereOn
fonte
3
@ Pubby: Não tenho certeza se esta é exatamente a mesma pergunta. A questão ligada é mais sobre "Devo pegar ...", enquanto meu foco pergunta sobre "Devo melhor captura ...ou <specific exception>antes rethrowing"
ereOn
53
Desculpe dizer isso, mas C ++ sem RAII não é C ++.
Fredoverflow
46
Portanto, seus colegas de trabalho descartam a técnica que foi inventada para lidar com um determinado problema e depois discutem sobre qual das alternativas inferiores deve ser usada? Lamento dizer, mas isso parece estúpido , não importa para que ponto de vista.
SBI
11
"pegar ... sem reler é realmente errado" - você está enganado. Em main, catch(...) { return EXIT_FAILURE; }pode estar correto no código que não está sendo executado em um depurador. Se você não pegar, a pilha pode não ser desenrolada. É somente quando seu depurador detecta exceções não capturadas que você deseja que ele saia main.
Steve Jessop
3
... portanto, mesmo que seja um "erro de programação", isso não significa necessariamente que você não queira saber. De qualquer forma, seus colegas não são bons profissionais de software, portanto, como o sbi diz que é muito difícil falar sobre a melhor forma de lidar com uma situação que é cronicamente fraca para começar.
quer

Respostas:

196

Meus colegas de trabalho dizem que você sempre deve saber quais exceções devem ser lançadas [...]

Seu colega de trabalho, detestaria dizer, obviamente nunca trabalhou em bibliotecas de uso geral.

Como no mundo uma classe pode std::vectoraté fingir saber o que os construtores de cópias lançarão, garantindo a segurança das exceções?

Se você sempre soubesse o que o receptor faria em tempo de compilação, o polimorfismo seria inútil! Às vezes, o objetivo inteiro é abstrair o que acontece em um nível inferior, para que você não queira saber o que está acontecendo!

Mehrdad
fonte
32
Na verdade, mesmo que soubessem que exceções devem ser lançadas. Qual é o objetivo dessa duplicação de código? A menos que o tratamento seja diferente, não vejo sentido enumerar as exceções para mostrar seu conhecimento.
Michael Krelin - hacker de
3
@ MichaelKrelin-hacker: Isso também. Além disso, adicione ao fato de que eles preteriram as especificações de exceção porque listar todas as exceções possíveis no código tende a causar erros mais tarde ... é a pior idéia de todos os tempos.
Mehrdad 5/12
4
E o que me incomoda é qual poderia ser a origem de tal atitude quando associada a ver uma técnica útil e conveniente como "material escolar inútil". Mas bem ...
Michael Krelin - hacker
1
+1, a enumeração de todas as opções possíveis é uma excelente receita para uma falha futura, por que alguém escolheria fazer isso novamente ...?
littleadv
2
Boa resposta. É possível que se mencione mencionar que, se um compilador que é necessário oferecer suporte possui um bug na área X, o uso da funcionalidade da área X não é inteligente, pelo menos para não usá-lo diretamente. Por exemplo, dadas as informações sobre a empresa, eu não ficaria surpreso se eles usassem o Visual C ++ 6.0, que tinha alguns erros estúpidos nessa área (como destruidores de objetos de exceção sendo chamados duas vezes) - alguns descendentes menores desses erros iniciais sobreviveram ao neste dia, mas exige cuidadosos arranjos para se manifestar.
Alf P. Steinbach
44

O que você parece ser pego é o inferno específico de alguém tentando comer seu bolo e também comê-lo.

RAII e exceções são projetadas para andar de mãos dadas. RAII é o meio pelo qual você não precisa escrever muitas catch(...)instruções para fazer a limpeza. Isso acontecerá automaticamente, como é óbvio. E as exceções são a única maneira de trabalhar com objetos RAII, porque os construtores só podem ter sucesso ou lançar (ou colocar o objeto em um estado de erro, mas quem quer isso?).

Uma catchdeclaração pode fazer uma de duas coisas: lidar com um erro ou circunstância excepcional ou executar um trabalho de limpeza. Às vezes, ele faz as duas coisas, mas catchexiste toda declaração para fazer pelo menos uma delas.

catch(...)é incapaz de executar um tratamento de exceção adequado. Você não sabe qual é a exceção; você não pode obter informações sobre a exceção. Você não tem absolutamente nenhuma informação além do fato de uma exceção ter sido lançada por algo dentro de um determinado bloco de código. A única coisa legítima que você pode fazer nesse bloco é fazer a limpeza. E isso significa repetir a exceção no final da limpeza.

O que RAII oferece a você em relação ao tratamento de exceções é a limpeza gratuita. Se tudo estiver encapsulado corretamente em RAII, tudo será limpo corretamente. Você não precisa mais ter catchinstruções para limpar. Nesse caso, não há razão para escrever uma catch(...)declaração.

Então, eu concordo que isso catch(...)é principalmente mau ... provisoriamente .

Essa disposição é o uso adequado da RAII. Porque sem ele, você precisa ser capaz de fazer determinada limpeza. Não há como fugir disso; você precisa ser capaz de fazer o trabalho de limpeza. Você precisa garantir que a emissão de uma exceção deixe o código em um estado razoável. E catch(...)é uma ferramenta vital para isso.

Você não pode ter um sem o outro. Você não pode dizer que ambos RAII e catch(...) são ruins. Você precisa de pelo menos um deles; caso contrário, você não será seguro para exceções.

Claro, há um uso válido, embora raro, catch(...)que nem a RAII pode banir: exception_ptrencaminhar para outra pessoa. Normalmente através de uma promise/futureinterface ou similar.

Meus colegas de trabalho dizem que você sempre deve saber quais exceções devem ser lançadas e que sempre pode usar construções como:

Seu colega de trabalho é um idiota (ou apenas terrivelmente ignorante). Isso deve ser imediatamente óbvio devido à quantidade de código de copiar e colar que ele sugere que você escreva. A limpeza para cada uma dessas instruções de captura será exatamente a mesma . Isso é um pesadelo de manutenção, sem mencionar a legibilidade.

Resumindo: esse é o problema que o RAII foi criado para resolver (não que ele não resolva outros problemas).

O que me confunde sobre essa noção é que geralmente é o contrário de como a maioria das pessoas argumenta que a RAII é ruim. Geralmente, o argumento diz "RAII é ruim porque você precisa usar exceções para sinalizar falha no construtor. Mas você não pode lançar exceções, porque não é seguro e terá que ter muitas catchinstruções para limpar tudo". O que é um argumento quebrado, porque o RAII resolve o problema que a falta de RAII cria.

Provavelmente, ele é contra a RAII porque esconde detalhes. As chamadas de destruidor não são visíveis imediatamente nas variáveis ​​automáticas. Então você obtém um código chamado implicitamente. Alguns programadores realmente odeiam isso. Aparentemente, até o ponto em que eles acham que ter três catchinstruções, todas as quais fazem o mesmo com o código de copiar e colar, é uma idéia melhor.

Nicol Bolas
fonte
2
Parece que você não escreve um código que forneça uma forte garantia de segurança de exceção. RAII ajuda a fornecer garantia básica . Mas, para fornecer uma garantia forte, é necessário desfazer algumas ações para reverter o sistema para o estado que ele tinha antes da função ser chamada. Garantia básica é limpeza , garantia forte é reversão . A reversão é específica da função. Então você não pode colocá-lo em "RAII". E é aí que o bloco catch-all se torna útil. Se você escrever código com garantia forte, você usa catch-all muito.
anton_rh
@anton_rh: Talvez, mas mesmo nesses casos, as instruções gerais sejam a ferramenta de último recurso . A ferramenta preferida é fazer tudo o que lança antes de alterar qualquer estado que você precisaria reverter com exceção. Obviamente, você não pode implementar tudo dessa maneira em todos os casos, mas essa é a maneira ideal de obter a forte garantia de exceção.
Nicol Bolas
14

Dois comentários, sério. A primeira é que, enquanto em um mundo ideal, você sempre deve saber quais exceções podem ser lançadas, na prática, se estiver lidando com bibliotecas de terceiros ou compilando com um compilador da Microsoft, não sabe. Mais ao ponto, no entanto; mesmo se você souber exatamente todas as possíveis exceções, isso é relevante aqui? catch (...)expressa a intenção muito melhor do que catch ( std::exception const& ), mesmo supondo que todas as exceções possíveis derivem de std::exception(o que seria o caso em um mundo ideal). Quanto ao uso de vários blocos de captura, se não houver uma base comum para todas as exceções: isso é ofuscação total e um pesadelo de manutenção. Como você reconhece que todos os comportamentos são idênticos? E essa era essa a intenção? E o que acontece se você precisar alterar o comportamento (correção de bug, por exemplo)? É muito fácil perder um.


fonte
3
Na verdade, meu colega de trabalho projetou sua própria classe de exceção, que não deriva std::exceptione tenta todos os dias impor seu uso entre nossa base de código. Meu palpite é que ele tenta me punir por usar código e bibliotecas externas que ele não escreveu.
ereOn
17
@ereOn Parece-me que seu colega de trabalho precisa urgentemente de treinamento. De qualquer forma, eu provavelmente evitaria usar bibliotecas escritas por ele.
2
Modelos e saber quais exceções serão lançadas combinam como manteiga de amendoim e lagartixas mortas. Algo tão simples quanto std::vector<>pode lançar qualquer tipo de exceção por praticamente qualquer motivo.
David Thornley
3
Por favor, diga-nos exatamente como você sabe que exceção será lançada pela correção de erros de amanhã mais adiante na árvore de chamadas?
mattnz
11

Acho que seu colega de trabalho confundiu alguns bons conselhos - você só deve lidar com exceções conhecidas em um catchbloco quando não as repetir.

Isso significa:

try
{
  // Stuff
}
catch (...)
{
  // General stuff
}

É ruim porque oculta silenciosamente qualquer erro.

Contudo:

try
{
  // Stuff
}
catch (exception_type_we_can_handle&)
{
  // Deal with the known exception
}

Está bom - sabemos com o que estamos lidando e não precisamos expô-lo ao código de chamada.

Da mesma forma:

try
{
  // Stuff
}
catch (...)
{
  // Rollback transactions, log errors, etc
  throw;
}

É bom, mesmo prática recomendada, o código para lidar com erros gerais deve estar com o código que os causa. É melhor do que confiar no chamado para saber que uma transação precisa ser revertida ou o que for.

Keith
fonte
9

Qualquer resposta de sim ou não deve ser acompanhada de uma justificativa do motivo.

Dizer que está errado, simplesmente porque me ensinaram dessa maneira, é apenas fanatismo cego.

Escrever o mesmo //Some cleanup; throwvárias vezes, como no seu exemplo, está errado porque é a duplicação de código e isso é uma carga de manutenção. Escrever apenas uma vez é melhor.

Escrever um catch(...)para silenciar todas as exceções está errado, porque você deve lidar apenas com as exceções que você sabe como lidar, e com esse curinga você pode fazer mais do que o esperado, e isso pode silenciar erros importantes.

Mas se você repetir catch(...)novamente após a , a última lógica não se aplicará mais, pois você não está realmente lidando com a exceção; portanto, não há razão para que isso seja desencorajado.

Na verdade, eu fiz isso para efetuar logon em funções sensíveis sem nenhum problema:

void DoSomethingImportant()
{
    try
    {
        Log("Going to do something important");
        DoIt();
    }
    catch (std::exception &e)
    {
        Log("Error doing something important: %s", e.what());
        throw;
    }
    catch (...)
    {
        Log("Unexpected error doing something important");
        throw;
    }
    Log("Success doing something important");
}
pestófago
fonte
2
Vamos esperar Log(...)que não possa jogar.
Deduplicator
2

Eu geralmente concordo com o humor das postagens aqui, eu realmente não gosto do padrão de capturar exceções específicas - acho que a sintaxe disso ainda está em sua infância e ainda não é capaz de lidar com o código redundante.

Mas como todo mundo está dizendo isso, vou me deparar com o fato de que, embora eu os use com moderação, muitas vezes olhei para uma das minhas declarações "catch (Exceção e)" e disse "Droga, eu gostaria de ter chamado fora as exceções específicas dessa época "porque, quando você chega mais tarde, geralmente é bom saber qual era a intenção e o que o cliente provavelmente lançaria de relance.

Não estou justificando a atitude de "Sempre use x", apenas dizendo que ocasionalmente é bom vê-los listados e tenho certeza que é por isso que algumas pessoas pensam que esse é o caminho "certo" a seguir.

Bill K
fonte