Por que as especificações de exceção são ruins?

50

De volta à escola, há mais de 10 anos, eles estavam ensinando você a usar especificadores de exceção. Como meu histórico é como um dos programadores Torvaldish C que evita teimosamente o C ++, a menos que seja forçado, eu só acabo no C ++ esporadicamente, e quando uso ainda uso especificadores de exceção, pois foi o que me ensinaram.

No entanto, a maioria dos programadores de C ++ parece desaprovar os especificadores de exceção. Eu li o debate e os argumentos de vários gurus de C ++, como estes . Tanto quanto eu entendo, tudo se resume a três coisas:

  1. Os especificadores de exceção usam um sistema de tipos que é inconsistente com o restante do idioma ("sistema de tipos de sombra").
  2. Se sua função com um especificador de exceção lançar outra coisa, exceto o que você especificou, o programa será encerrado de maneiras ruins e inesperadas.
  3. Os especificadores de exceção serão removidos no próximo padrão C ++.

Estou faltando alguma coisa aqui ou essas são todas as razões?

Minhas próprias opiniões:

Em relação a 1): E daí? C ++ é provavelmente a linguagem de programação mais inconsistente já criada, em termos de sintaxe. Temos as macros, os goto / labels, a horda (horda?) De comportamento indefinido / não especificado / definido pela implementação, os tipos inteiros mal definidos, todas as regras implícitas de promoção de tipos, palavras-chave implícitas de promoção, palavras-chave de caso especial como friend, auto , registrar, explícito ... E assim por diante. Alguém provavelmente poderia escrever vários livros grossos de toda a estranheza em C / C ++. Então, por que as pessoas estão reagindo contra essa inconsistência específica, que é uma falha menor em comparação com muitas outras características muito mais perigosas da linguagem?

Em relação a 2): Essa não é minha responsabilidade? Existem muitas outras maneiras de escrever um bug fatal em C ++. Por que esse caso em particular é pior? Em vez de escrever throw(int)e depois lançar Crash_t, posso também afirmar que minha função retorna um ponteiro para int, depois cria um tipo de letra explícito e selvagem e retorna um ponteiro para um Crash_t. O espírito do C / C ++ sempre foi deixar a maior parte da responsabilidade para o programador.

E as vantagens, então? O mais óbvio é que, se sua função tentar lançar explicitamente qualquer tipo diferente do que você especificou, o compilador apresentará um erro. Eu acredito que o padrão é claro em relação a isso (?). Os erros só acontecem quando sua função chama outras funções que, por sua vez, lançam o tipo errado.

Vindo de um mundo de programas C determinísticos e incorporados, eu certamente preferiria saber exatamente o que uma função jogará para mim. Se houver algo no idioma que suporte isso, por que não usá-lo? As alternativas parecem ser:

void func() throw(Egg_t);

e

void func(); // This function throws an Egg_t

Acho que há uma grande chance de o chamador ignorar / esquecer de implementar o try-catch no segundo caso, menos no primeiro caso.

Pelo que entendi, se uma dessas duas formas decidir lançar repentinamente outro tipo de exceção, o programa falhará. No primeiro caso, porque não é permitido lançar outra exceção, no segundo caso, porque ninguém esperava lançar um SpanishInquisition_t e, portanto, essa expressão não é capturada onde deveria estar.

No caso deste último, ter algumas capturas de último recurso (...) no nível mais alto do programa não parece realmente melhor do que uma falha no programa: "Ei, em algum lugar do seu programa, algo lançou uma exceção estranha e sem tratamento . " Você não pode recuperar o programa quando estiver longe de onde a exceção foi lançada, a única coisa que você pode fazer é sair do programa.

E, do ponto de vista do usuário, eles não se importariam se recebessem uma caixa de mensagens maliciosas do sistema operacional dizendo "Programa encerrado. Blablabla no endereço 0x12345" ou uma caixa de mensagens maliciosas do seu programa dizendo "Exceção não tratada: minha classe. func.something ". O bug ainda está lá.


Com o próximo padrão C ++, não terei outra opção senão abandonar os especificadores de exceção. Mas eu preferiria ouvir algum argumento sólido sobre por que eles são ruins, em vez de "Sua Santidade declarou isso e é assim". Talvez haja mais argumentos contra eles do que os que listei, ou talvez haja mais deles do que imagino?


fonte
30
Estou tentado a rebaixar isso como "discurso retórico, disfarçado de pergunta". Você pergunta três pontos válidos sobre a E.specs, mas por que você nos incomoda com suas divagações em C ++ - é tão irritante e eu pareço C?
Martin Ba
10
@ Martin: Porque quero ressaltar que sou ao mesmo tempo tendencioso e não atualizado com todos os detalhes, mas também que considero a linguagem com olhos ingênuos e / ou ainda não arruinados. Mas também que C e C ++ já são linguagens incrivelmente falhas, portanto uma falha mais ou menos não importa. O post era realmente muito pior antes de eu editá-lo :) Os argumentos contra os especificadores de exceção também são bastante subjetivos e, portanto, é difícil discuti-los sem ser subjetivo.
SpanishInquisition_t! Hilário! Pessoalmente, fiquei impressionado com o uso do ponteiro de quadro de pilha para lançar exceções e parece que ele pode tornar o código muito mais limpo. No entanto, nunca realmente escrevi código com exceções. Me chame de antiquado, mas os valores de retorno funcionam muito bem para mim.
Shahbaz
@ Shahbaz Como se pode ler nas entrelinhas, também sou antiquado, mas ainda assim nunca questionei se as exceções são boas ou más.
2
O passado de lance é jogou , não jogou . É um verbo forte.
Trig

Respostas:

48

As especificações de exceção são ruins porque são pouco aplicadas e, portanto, não realizam muito, e também são ruins porque forçam o tempo de execução a procurar exceções inesperadas para que possam terminar (), em vez de invocar o UB , isso pode desperdiçar uma quantidade significativa de desempenho.

Portanto, em resumo, as especificações de exceção não são aplicadas com força suficiente no idioma para tornar o código mais seguro, e implementá-las conforme especificado foi uma grande perda de desempenho.

DeadMG
fonte
2
Mas a verificação em tempo de execução não é culpa da especificação de exceção, mas sim de qualquer parte do padrão que declara que deve haver uma finalização se o tipo incorreto for lançado. Não seria mais inteligente simplesmente remover o requisito que leva a essa verificação dinâmica e permitir que tudo seja avaliado estaticamente pelo compilador? Parece que o RTTI é o verdadeiro culpado aqui, ou ...?
7
@Lundin: não é possível determinar estaticamente todas as exceções que poderiam ser lançadas de uma função em C ++, porque a linguagem permite alterações arbitrárias e não determinísticas no fluxo de controle em tempo de execução (por exemplo, através de ponteiros de função, métodos virtuais e o gosto). A implementação da verificação estática de exceção em tempo de compilação, como Java, exigiria alterações fundamentais na linguagem e muita compatibilidade com C seria desativada.
3
@OrbWeaver: Eu acho que as verificações estáticas são bem possíveis - a especificação de exceção faria parte da especificação de uma função (como o valor de retorno), e os ponteiros de função, substituições virtuais, etc. teriam que ter especificações compatíveis. A principal objeção seria que é um recurso tudo ou nada - funções com especificações de exceção estáticas não poderiam chamar funções herdadas. (Chamar funções C seria bom, pois elas não podem lançar nada).
Mike Seymour
2
@Lundin: as especificações de exceção não foram removidas, apenas obsoletas. O código legado que os utiliza ainda será válido.
Mike Seymour
11
@Lundin: Versões antigas do C ++ com especificações de exceção são perfeitamente compatíveis. Eles não falharão na compilação nem na execução inesperada se o código tiver sido válido antes.
23411 DeadMG
18

Uma razão pela qual ninguém as usa é porque sua suposição primária está errada:

"A [vantagem] mais óbvia é que, se sua função tentar lançar explicitamente qualquer tipo diferente do que você especificou, o compilador apresentará um erro."

struct foo {};
struct bar {};

struct test
{
    void baz() throw(foo)
    {
        throw bar();
    }
};

int main()
{
    test x;
    try { x.baz(); } catch(bar &b) {}
}

Este programa é compilado sem erros ou avisos .

Além disso, mesmo que a exceção tivesse sido detectada , o programa ainda é encerrado.


Editar: para responder a um ponto da sua pergunta, pegar (...) (ou melhor, pegar (std :: exeption &) ou outra classe base e depois pegar (...)) ainda é útil, mesmo que você não o faça. saber exatamente o que deu errado.

Considere que seu usuário apertou um botão de menu para "salvar". O manipulador do botão de menu convida o aplicativo a salvar. Isso pode falhar por inúmeras razões: o arquivo estava em um recurso de rede que desapareceu, havia um arquivo somente leitura e não pôde ser salvo, etc. Mas o manipulador realmente não se importa por que algo falhou; só se preocupa se conseguiu ou falhou. Se conseguiu, tudo está bem. Se falhar, pode informar ao usuário. A natureza exata da exceção é irrelevante. Além disso, se você escrever um código adequado e seguro para exceções, isso significa que qualquer erro desse tipo pode se propagar pelo sistema sem derrubá-lo - mesmo para códigos que não se importam com o erro. Isso facilita a extensão do sistema. Por exemplo, agora você salva por meio de uma conexão com o banco de dados.

Kaz Dragon
fonte
4
@Lundin compilou sem avisos. E não, você não pode. Não é confiável. Você pode chamar através de ponteiros de função ou pode ser uma função de membro virtual, e a classe derivada lança derivada_exception (que a classe base não pode conhecer).
quer
Muita sorte. A Embarcadero / Borland alerta: "a exceção de lançamento viola o especificador de exceção". Aparentemente, o GCC não. Não há razão para que eles não possam implementar um aviso. E da mesma forma, uma ferramenta de análise estática pode facilmente encontrar esse bug.
14
@Lundin: Por favor, não comece a discutir e repetir informações falsas. Sua pergunta já era uma espécie de discurso retórico, e alegar coisas falsas não ajuda. Uma ferramenta de análise estática NÃO PODE descobrir se isso acontece; fazer isso envolveria resolver o problema da parada. Além disso, seria necessário analisar todo o programa e, muitas vezes, toda a fonte não está disponível.
precisa
11
@Lundin a mesma ferramenta de análise estática poderia dizer quais exceções lançadas deixam sua pilha, portanto, nesse caso, o uso de especificações de exceção ainda não comprará nada, exceto um potencial falso negativo que derrubará seu programa onde você poderia ter tratado o caso de erro em de uma maneira muito mais agradável (como anunciar a falha e continuar executando: veja a segunda metade da minha resposta como exemplo).
quer
11
@Lundin Uma boa pergunta. A resposta é que, em geral, você não sabe se as funções que você está chamando lançarão exceções; portanto, a suposição segura é que todas elas funcionam. Onde pegá-las é uma pergunta que eu respondi em stackoverflow.com/questions/4025667/… . Os manipuladores de eventos, em particular, formam limites naturais do módulo. Permitir que exceções escapem para um sistema genérico de janela / evento (por exemplo) será punido. O manipulador deve pegá-los todos se o programa sobreviver lá.
Kaz Dragon
17

Esta entrevista com Anders Hejlsberg é bastante famosa. Nele, ele explica por que a equipe de design do C # descartou exceções verificadas em primeiro lugar. Em poucas palavras, existem dois motivos principais: capacidade de versão e escalabilidade.

Eu sei que o OP está focado em C ++, enquanto Hejlsberg está discutindo C #, mas os pontos que Hejlsberg destaca também são perfeitamente aplicáveis ​​a C ++.

CesarGon
fonte
5
"os argumentos de Hejlsberg também são perfeitamente aplicáveis ​​ao C ++" .. Errado! As exceções verificadas implementadas em Java forçam o chamador a lidar com elas (e / ou o chamador, etc.). As especificações de exceção em C ++ (embora bastante fracas e inúteis) se aplicam apenas a um único "limite de chamada de função" e não "propagam a pilha de chamadas", como as exceções verificadas.
Martin Ba
3
@ Martin: Eu concordo com você sobre o comportamento das especificações de exceção em C ++. Mas minha frase que você cita "os pontos que Hejlsberg destaca também é perfeitamente aplicável ao C ++" refere-se aos pontos que Hejlsberg destaca, em vez das especificações do C ++. Em outras palavras, estou falando aqui sobre capacidade de versão e escalabilidade do programa, em vez de propagação de exceção.
CesarGon
3
Eu discordei de Hejlsberg bem cedo: "A preocupação que tenho com exceções verificadas é a algema que eles colocam nos programadores. Você vê os programadores pegando novas APIs que possuem todas essas cláusulas de arremesso e depois vê como o código fica complicado, e você percebe as exceções verificadas não estão ajudando nenhuma. São esses designers ditatoriais da API que lhe dizem como lidar com as exceções ". --- Exceções sendo verificadas ou não, você ainda precisa lidar com as exceções geradas pela API que está chamando. As exceções verificadas simplificam explicitamente.
2261111
5
@quant_dev: Não, você não precisa lidar com as exceções geradas pela API que está chamando. Uma opção perfeitamente válida é não lidar com as exceções e permitir que elas aumentem a pilha de chamadas para o chamador lidar.
CesarGon
4
@CesarGon Não lidar com exceções também está fazendo uma escolha de como lidar com elas.
2241111