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

74

Embora eu concorde que pegar ... sem voltar a retroceder esteja de fato errado, eu acredito que usando construções como essa:

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

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

Meus colegas de trabalho dizem que você deve sempre 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 aceita em relação a essas situações?

    
por ereOn 05.12.2011 / 10:05
fonte

6 respostas

189

My coworkers says that you should always know what exceptions are to be thrown [...]

Seu colega de trabalho, eu odiaria dizer isso, obviamente nunca trabalhou em bibliotecas de propósito geral.

Como no mundo uma classe como std::vector pode fingir saber o que os construtores de cópia lançarão, garantindo ao mesmo tempo segurança de exceção?

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

    
por 05.12.2011 / 10:07
fonte
43

O que você parece estar preso é o inferno específico de alguém tentando ter seu bolo e comê-lo também.

RAII e exceções são projetadas para andar de mãos dadas. RAII é o meio pelo qual você não tem para gravar muitas instruções catch(...) 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 instrução catch pode fazer uma das duas coisas: manipular um erro ou uma circunstância excepcional ou fazer um trabalho de limpeza. Às vezes, ele faz as duas coisas, mas todas as instruções catch existem para fazer pelo menos uma delas.

catch(...) é incapaz de fazer o tratamento adequado de exceções. 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 que uma exceção foi lançada por algo dentro de um determinado bloco de código. A única coisa legítima que você pode fazer nesse tipo de bloqueio é fazer a limpeza. E isso significa re-lançar a exceção no final da limpeza.

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

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

Essa disposição é o uso adequado do RAII. Porque sem ele, você precisa para poder fazer certas limpezas. Não há como fugir disso; você tem que ser capaz de fazer o trabalho de limpeza. Você precisa ser capaz de garantir que lançar uma exceção deixará o código em um estado razoável. E catch(...) é uma ferramenta vital ao fazer 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 desses; caso contrário, você não é uma exceção segura.

Claro, há um uso válido, embora raro, de catch(...) que nem mesmo o RAII pode banir: recebendo um exception_ptr para encaminhar para outra pessoa. Normalmente, através de uma interface promise/future ou similar.

My coworkers says that you should always know what exceptions are to be thrown and that you can always use constructs like:

Seu colega de trabalho é um idiota (ou simplesmente ignorante). Isso deve ser imediatamente óbvio devido à quantidade de código para copiar e colar que ele sugere que você escreva. A limpeza de cada uma dessas declarações catch será exatamente a mesma . Isso é um pesadelo de manutenção, para não 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 é de trás para a forma como a maioria das pessoas argumenta que RAII é ruim. Geralmente, o argumento é "RAII é ruim porque você tem que usar exceções para sinalizar falha de construtor. Mas você não pode lançar exceções, porque não é seguro e você terá que ter muitas instruções catch para limpar tudo. " Qual é um argumento quebrado porque o RAII resolve o problema que a falta de RAII cria.

Mais do que provável, ele é contra RAII porque esconde detalhes. Chamadas de destruidor não são imediatamente visíveis em variáveis automáticas. Então você pega o código que é chamado implicitamente. Alguns programadores realmente odeiam isso. Aparentemente, ao ponto em que eles acham que ter 3% de declaraçõescatch, todas fazem a mesma coisa com o código copiar e colar, é uma idéia melhor.

    
por 05.12.2011 / 12:00
fonte
14

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

    
por 05.12.2011 / 10:21
fonte
11

Acho que seu colega de trabalho misturou alguns bons conselhos - você só deve lidar com exceções conhecidas em um bloco catch quando não voltar a jogá-los.

Isso significa:

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

É ruim porque ocultará silenciosamente qualquer erro

.

No entanto:

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

Está bem - sabemos com o que estamos lidando e não precisamos expor isso ao código de chamada.

Da mesma forma:

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

Está tudo bem, a melhor prática, até mesmo, o código para lidar com erros gerais deve ser com o código que os causa. É melhor do que confiar no candidato saber que uma transação precisa ser revertida ou algo assim.

    
por 05.12.2011 / 11:27
fonte
9

Qualquer resposta de sim ou no deve ser acompanhada de uma justificativa do motivo.

Dizer que está errado simplesmente porque fui ensinado dessa maneira é apenas fanatismo cego.

Escrever o mesmo //Some cleanup; throw várias vezes, como no seu exemplo, está errado porque há duplicação de código e isso é um fardo de manutenção. Escrevê-lo apenas uma vez é melhor.

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

Mas se você relançar após um catch(...) , o último argumento não se aplica mais, pois você não está lidando com a exceção, então não há razão para que isso seja desencorajado.

Na verdade, fiz isso para fazer login em funções confidenciais sem qualquer 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");
}
    
por 05.12.2011 / 10:32
fonte
2

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

Mas como todo mundo está dizendo isso, eu vou falar com o fato de que mesmo que eu os use com parcimônia, eu sempre olhei para uma das minhas declarações de "pegar (Exception e)" e disse "Droga, eu desejo Eu indiquei as exceções específicas que o tempo ", porque quando você entra mais tarde, é sempre bom saber qual era a intenção e o que o cliente provavelmente lançaria de relance.

Eu não estou justificando a atitude de "Always use x", apenas dizendo que ocasionalmente é bom vê-los listados e tenho certeza que é por isso que algumas pessoas acham que é o caminho "certo" para seguir. / p>     

por 05.12.2011 / 19:05
fonte

Tags