Try-catch ou ifs para tratamento de erros em C ++

30

As exceções são amplamente usadas no design de mecanismos de jogos ou é mais preferível usar instruções if puras? Por exemplo, com exceções:

try {
    m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
    m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
    m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
    m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
    m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
    m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );
} catch ... {
    // some info about error
}

e com ifs:

m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
if( m_fpsTextId == -1 ) {
    // show error
}
m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
if( m_cpuTextId == -1 ) {
    // show error
}
m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
if( m_frameTimeTextId == -1 ) {
    // show error
}
m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
if( m_mouseCoordTextId == -1 ) {
    // show error
}
m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
if( m_cameraPosTextId == -1 ) {
    // show error
}
m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );
if( m_cameraRotTextId == -1 ) {
    // show error
}

Ouvi dizer que as exceções são um pouco mais lentas que as ifs e não devo misturar exceções com o método de verificação de if. No entanto, com exceções, tenho um código mais legível do que com toneladas de ifs após cada método initialize () ou algo semelhante, embora eles às vezes sejam muito pesados ​​para apenas um método na minha opinião. Eles estão bem para o desenvolvimento de jogos ou melhor, ficar com ifs simples?

tobi
fonte
Muitas pessoas afirmam que o Boost é ruim para o desenvolvimento de jogos, mas eu recomendo que você analise ["Boost.optional"] ( boost.org/doc/libs/1_52_0/libs/optional/doc/html/index.html ) seu exemplo ifs um pouco melhor
ManicQin 30/12/12
Há uma boa conversa sobre o tratamento de erros por Andrei Alexandrescu, usei uma abordagem semelhante à dele, sem exceções.
Thelvyn

Respostas:

48

Resposta curta: leia Tratamento sensível a erros 1 , Tratamento sensível a erros 2 e Tratamento sensível a erros 3 por Niklas Frykholm. Na verdade, leia todos os artigos desse blog enquanto estiver nele. Não vou dizer que concordo com tudo, mas a maioria é de ouro.

Não use exceções. Há uma infinidade de razões. Vou listar os principais.

Eles podem realmente ser mais lentos, embora isso seja bastante minimizado em compiladores mais recentes. Alguns compiladores suportam "zero exceções de sobrecarga" para caminhos de código que realmente não acionam exceção (embora isso seja um pouco mentiroso, pois ainda existem dados extras que o tratamento de exceções exige, inchando o tamanho do executável / dll). O resultado final é que sim, o uso de exceções é mais lento e, em qualquer desempenho crítico caminho de código , você deve evitá-las. Dependendo do seu compilador, tê-los ativados pode adicionar sobrecarga. Eles sempre aumentam o tamanho do código, de maneira bastante significativa em muitos casos, o que pode afetar seriamente o desempenho do hardware atual.

Exceções tornam o código muito mais frágil. Existe um gráfico infame (que infelizmente não consigo encontrar no momento) que basicamente apenas mostra em um gráfico a dificuldade de escrever código com exceção de segurança versus código com exceção de inseguro, e o primeiro é uma barra significativamente maior. Existem simplesmente algumas pequenas dicas com exceções e muitas maneiras de escrever código que parece excepcionalmente seguro, mas realmente não é. Mesmo todo o comitê do C ++ 11 brincou com esse e esqueceu de adicionar funções auxiliares importantes para o uso correto do std :: unique_ptr, e mesmo com essas funções auxiliares, é preciso digitar mais para usá-las do que não e a maioria dos programadores venceu ' nem percebe o que há de errado se não o fizerem.

Mais especificamente para a indústria de jogos, alguns compiladores / tempos de execução fornecidos pelos fornecedores dos consoles não suportam totalmente as exceções, ou mesmo nem sequer as suportam. Se seu código usa exceções, você ainda pode reescrever partes do seu código para portá-lo para novas plataformas. (Não tenho certeza se isso foi alterado nos 7 anos desde que os consoles foram lançados; simplesmente não usamos exceções, elas são desativadas nas configurações do compilador, por isso não sei se alguém com quem conversei verificou recentemente.)

A linha de pensamento geral é bastante clara: use exceções para circunstâncias excepcionais . Use-os quando o seu programa entrar em um "Eu não tenho idéia do que fazer, talvez espero que alguém o faça, então vou abrir uma exceção e ver o que acontece". Use-os quando nenhuma outra opção fizer sentido. Use-os quando não se importar se você vazar acidentalmente um pouco de memória ou falhar na limpeza de um recurso porque você estragou o uso de identificadores inteligentes adequados. Em todos os outros casos, não os use.

Em relação a códigos como o seu exemplo, você tem várias outras maneiras de corrigir o problema. Um dos mais robustos - embora não necessariamente o mais ideal em seu exemplo simples - é inclinar-se para os tipos de erros monádicos. Ou seja, createText () pode retornar um tipo de identificador personalizado em vez de um número inteiro. Esse tipo de identificador possui acessadores para atualizar ou controlar o texto. Se o identificador for colocado em um estado de erro (porque createText () falhou), outras chamadas para o identificador simplesmente falharão silenciosamente. Você também pode consultar o identificador para ver se houve algum erro e, em caso afirmativo, qual foi o último erro. Essa abordagem tem mais sobrecarga do que outras opções, mas é bastante sólida. Use-o nos casos em que você precise executar uma longa sequência de operações em algum contexto em que uma única operação possa falhar na produção, mas onde você não / não pode / venceu '

Uma alternativa para implementar a manipulação de erros monádicos é, em vez de usar objetos de manipulação personalizados, fazer com que os métodos no objeto de contexto lidem normalmente com IDs de manipulação inválidos. Por exemplo, se createText () retornar -1 quando falhar, quaisquer outras chamadas para m_statistics que usam uma dessas alças devem sair normalmente se -1 for passado.

Da mesma forma, você pode colocar o erro de impressão dentro da função que está realmente falhando. No seu exemplo, o createText () provavelmente possui muito mais informações sobre o que deu errado, portanto poderá despejar um erro mais significativo no log. Neste caso, há pouco benefício em enviar o tratamento / impressão de erros aos chamadores. Faça isso quando os chamadores precisarem personalizar o tratamento (ou usar a injeção de dependência). Observe que ter um console no jogo que pode aparecer sempre que um erro é registrado é uma boa ideia e ajuda aqui também.

A melhor opção (já apresentada na série vinculada de artigos acima) para chamadas que você não espera falhar em nenhum ambiente sadio - como o simples ato de criar blobs de texto para um sistema de estatísticas - é apenas ter a função que falhou (createText no seu exemplo) abortar. Você pode ter certeza razoável de que createText () não falhará na produção, a menos que algo seja totalmente destruído (por exemplo, o usuário excluiu os arquivos de dados da fonte ou, por algum motivo, tenha apenas 256 MB de memória etc.). Em muitos desses casos, não há nem uma coisa boa a fazer quando ocorre uma falha. Fora da memória? Talvez você nem consiga fazer uma alocação necessária para criar um bom painel da GUI para mostrar ao usuário o erro OOM. Faltando fontes? Torna difícil exibir erros para o usuário. O que está errado,

Apenas travar é totalmente bom, desde que você (a) registre o erro em um arquivo de log e (b) faça apenas em erros que não são causados ​​por ações regulares do usuário.

Eu não diria o mesmo para muitos aplicativos de servidor, onde a disponibilidade é crítica e o monitoramento de vigilância não é suficiente, mas isso é bem diferente do desenvolvimento do cliente do jogo . Eu também evitaria o uso do C / C ++ lá, pois os recursos de tratamento de exceções de outras linguagens tendem a não morder como o C ++, pois são ambientes gerenciados e não têm todos os problemas de segurança de exceção que o C ++ possui. Quaisquer preocupações de desempenho também são atenuadas, pois os servidores tendem a se concentrar mais em paralelismo e taxa de transferência do que as garantias mínimas de latência, como os clientes do jogo. Até servidores de jogos de ação para atiradores e similares podem funcionar muito bem quando escritos em C #, por exemplo, uma vez que raramente levam o hardware ao limite, como costumam fazer os clientes de FPS.

Sean Middleditch
fonte
1
+1. Além disso, existe a possibilidade de adicionar atributos como warn_unused_resulta função em alguns compiladores, o que permite capturar código de erro não tratado.
Maciej Piechotka
Vim aqui vendo C ++ no título e tinha total certeza de que o tratamento de erros monádicos já não estaria aqui. Coisa boa!
Adam
o que dizer de lugares onde o desempenho não é crítico e você está buscando uma implementação rápida? por exemplo, quando você está implementando a lógica do jogo?
precisa saber é o seguinte
e a idéia de usar o tratamento de exceções apenas para fins de depuração? como quando você lança o jogo, espera que tudo corra bem, sem problemas. portanto, você usa exceções para localizar e corrigir erros durante o desenvolvimento e, posteriormente, no modo de liberação, remove todas essas exceções.
precisa saber é o seguinte
1
@ Gajoo: para a lógica do jogo, as exceções apenas tornam a lógica mais difícil de seguir (pois torna todo o código mais difícil de seguir). Mesmo em Pythnn e C #, as exceções são raras para a lógica do jogo, na minha experiência. para depuração, a afirmação definitiva é geralmente muito mais conveniente. Quebre e pare no momento exato em que algo der errado, não depois de desenrolar grandes partes da pilha e perder todos os tipos de informações contextuais devido à semântica de lançamento de exceções. Para a lógica de depuração, você deseja criar ferramentas e informações / editar GUIs, para que os designers possam inspecionar e ajustar.
Sean Middleditch
13

A velha sabedoria do próprio Donald Knuth é:

"Devemos esquecer pequenas ineficiências, digamos, 97% das vezes: a otimização prematura é a raiz de todo mal".

Imprima isso como um cartaz grande e pendure-o em qualquer lugar onde você faça uma programação séria.

Portanto, mesmo se try / catch for um pouco mais lento, eu o usaria por padrão:

  • O código deve ser o mais legível e compreensível possível. Você pode escrever o código uma vez, mas você vai lê-lo muitas mais vezes para depurar este código, para depuração de outro código, para entender, para a melhoria, ...

  • Correção sobre o desempenho. Fazer as coisas if / else corretamente não é trivial. No seu exemplo, isso é feito incorretamente, porque nenhum erro pode ser mostrado, mas vários. Você precisa usar em cascata se / then / else.

  • Mais fácil de usar de forma consistente: o try / catch adota um estilo em que você não precisa verificar se há erros em todas as linhas. Por outro lado: Um faltando if / else e seu poder de código destruir estragos. Obviamente, apenas em circunstâncias improdutíveis.

Então, o último ponto:

Ouvi dizer que as exceções são um pouco mais lentas que as ifs

Ouvi dizer que cerca de 15 anos atrás, não ouvi isso de nenhuma fonte confiável recentemente. Compiladores podem ter melhorado ou o que for.

O ponto principal é: Não faça otimização prematura. Faça isso apenas quando puder provar por referência que o código em questão é o loop interno apertado de algum código muito usado e que o estilo de alternância melhora consideravelmente o desempenho . Este é o caso de 3%.

AH
fonte
22
Vou -1 isso até o infinito. Não consigo entender o mal que essa citação de Knuth causou aos cérebros dos desenvolvedores. Considerando se o uso de exceções não é uma otimização prematura, é uma decisão de design, uma decisão importante de design que tem ramificações muito além do desempenho.
31512 Sam_Hocevar
3
@ SamHocevar: Sinto muito, mas não entendi seu ponto de vista: a questão é sobre o possível impacto no desempenho ao usar exceções. O que quero dizer: não pense nisso, o golpe não é tão ruim (se houver) e outras coisas são muito mais importantes. Aqui você parece concordar adicionando "decisão de design" à minha lista. ESTÁ BEM. Por outro lado, você diz que a citação de Knuth é ruim, o que implica que a otimização prematura não é ruim. Mas foi exatamente o que aconteceu aqui. IMO: O Q não pensa em arquitetura, design ou algoritmos diferentes, apenas em exceções e em seu impacto no desempenho.
AH
2
Eu discordo da clareza em C ++. O C ++ possui exceções desmarcadas, enquanto alguns compiladores implementam valores de retorno verificados. Como resultado, você tem um código que parece mais claro, mas pode ter vazamento de memória oculto ou até colocar código em estado indefinido. Também código de terceiros pode não ser seguro para exceções. (A situação é diferente em Java / C # que verificou exceções, GC, ...). Como decisão de projeto - se eles não cruzam os pontos da API, a refatoração de / para cada estilo pode ser feita de maneira semi-automática com linhas de comando perl.
Maciej Piechotka
1
@ SamHocevar: " A pergunta é sobre o que é preferível, não o que é mais rápido " . Releia o último parágrafo da pergunta. A única razão pela qual ele está pensando em não usar exceções é porque ele acha que elas podem ser mais lentas. Agora, se você acha que há outras preocupações que o OP não levou em consideração, sinta-se à vontade para publicá-las ou aprovar aquelas que o fizeram. Mas o OP está claramente focado no desempenho de exceções.
Nicol Bolas
3
@AH - se você quiser citar Knuth, não esqueça o resto (e a IMO é a parte mais importante): " No entanto, não devemos desperdiçar nossas oportunidades nesses 3% críticos. Um bom programador não será embalado em complacência por esse raciocínio, ele será sábio em examinar atentamente o código crítico; mas somente depois que esse código for identificado . " Com muita frequência, vemos isso como uma desculpa para não otimizar, ou para tentar justificar que o código seja lento nos casos em que o desempenho não é uma otimização, mas é de fato um requisito fundamental.
Maximus Minimus
10

Já houve uma resposta realmente ótima para essa pergunta, mas gostaria de acrescentar algumas idéias sobre a legibilidade do código e a recuperação de erros no seu caso específico.

Acredito que seu código possa ter a seguinte aparência:

m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );

Sem exceções, sem ifs. O relatório de erros pode ser feito em createText. E createTextpode retornar um ID de textura padrão que não exige que você verifique o valor de retorno, para que o restante do código funcione da mesma forma.

sam hocevar
fonte