Try-catch o ifs para manejo de errores en C ++

30

¿Se usan ampliamente las excepciones en el diseño del motor de juego o es más preferible usar declaraciones if? Por ejemplo, con excepciones:

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
}

y con 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
}

Escuché que las excepciones son un poco más lentas que ifs y no debería mezclar excepciones con el método if-check. Sin embargo, con excepciones, tengo un código más legible que con toneladas de ifs después de cada método initialize () o algo similar, aunque en mi opinión, a veces son demasiado pesados ​​para un solo método. ¿Están bien para el desarrollo del juego o mejor para seguir con simples ifs?

tobi
fuente
Muchas personas afirman que Boost es malo para el desarrollo del juego, pero le recomiendo que mire ["Boost.optional"] ( boost.org/doc/libs/1_52_0/libs/optional/doc/html/index.html ) lo hará su ejemplo de ifs es un poco mejor
ManicQin
Hay una buena charla sobre el manejo de errores por Andrei Alexandrescu, utilicé un enfoque similar al suyo sin excepciones.
Thelvyn

Respuestas:

48

Respuesta corta: lea Manejo del error sensible 1 , Manejo del error sensible 2 y Manejo del error sensible 3 por Niklas Frykholm. En realidad, lea todos los artículos en ese blog, mientras lo hace. No diré que estoy de acuerdo con todo, pero la mayor parte es oro.

No uses excepciones. Hay una gran cantidad de razones. Voy a enumerar los más importantes.

De hecho, pueden ser más lentos, aunque eso se minimiza bastante en los compiladores más nuevos. Algunos compiladores admiten "excepciones de sobrecarga cero" para las rutas de código que en realidad no desencadenan excepciones (aunque eso es un poco mentiroso, ya que todavía hay datos adicionales que requiere el manejo de excepciones, inflando su tamaño ejecutable / dll). Sin embargo, el resultado final es que sí, el uso de excepciones es más lento, y en cualquier ruta de código crítica de rendimiento , debe evitarlas por completo. Dependiendo de su compilador, tenerlos habilitados puede agregar una sobrecarga. Siempre hinchan el tamaño del código, bastante significativamente en muchos casos, lo que puede afectar gravemente el rendimiento en el hardware actual.

Las excepciones hacen que el código sea mucho más frágil. Hay un gráfico infame (que lamentablemente no puedo encontrar en este momento) que básicamente solo muestra en un gráfico la dificultad de escribir código seguro de excepción frente a código inseguro de excepción, y el primero es una barra significativamente más grande. Simplemente hay muchas pequeñas trampas con excepciones, y muchas formas de escribir código que parece una excepción segura pero realmente no lo es. Incluso todo el comité de C ++ 11 se burló de este y olvidó agregar una función auxiliar importante para usar std :: unique_ptr correctamente, e incluso con esas funciones auxiliares, se necesita más tipeo para usarlas que la mayoría de los programadores ' Ni siquiera te das cuenta de lo que está mal si no lo hacen.

Más específicamente para la industria de los juegos, los compiladores / tiempos de ejecución proporcionados por el proveedor de algunas consolas directamente no admiten excepciones completamente, o incluso no las admiten en absoluto. Si su código usa excepciones, es posible que aún hoy en día tenga que reescribir partes de su código para portarlo a nuevas plataformas. (No estoy seguro de si eso ha cambiado en los 7 años transcurridos desde que se lanzaron dichas consolas; simplemente no usamos excepciones, incluso las deshabilitamos en la configuración del compilador, por lo que no sé si alguien con quien hablo lo haya comprobado recientemente).

La línea de pensamiento general es bastante clara: use excepciones para circunstancias excepcionales . Úselos cuando su programa entre en un "No tengo idea de qué hacer, tal vez espero que alguien más lo haga, así que lanzaré una excepción y veré qué sucede". Úselos cuando ninguna otra opción tenga sentido. Úselos cuando no le importe si accidentalmente pierde un poco de memoria o no puede limpiar un recurso porque estropeó el uso de manijas inteligentes adecuadas. En todos los demás casos, no los use.

Con respecto al código como su ejemplo, tiene varias otras formas de solucionar el problema. Uno de los más robustos, aunque no necesariamente el más ideal en su ejemplo simple, es inclinarse hacia los tipos de error monádico. Es decir, createText () puede devolver un tipo de identificador personalizado en lugar de un entero. Este tipo de identificador tiene accesores para actualizar o controlar el texto. Si el identificador se coloca en un estado de error (debido a que createText () falló), otras llamadas al identificador simplemente fallan en silencio. También puede consultar el identificador para ver si se ha producido un error y, de ser así, cuál fue el último error. Este enfoque tiene más gastos generales que otras opciones, pero es bastante sólido. Úselo en los casos en que necesite realizar una larga serie de operaciones en algún contexto en el que cualquier operación individual podría fallar en la producción, pero donde no puede / no puede / ganó '

Una alternativa para implementar el manejo de errores monádicos es, en lugar de usar objetos de manejo personalizados, hacer que los métodos en el objeto de contexto traten con gracia los identificadores de manejo no válidos. Por ejemplo, si createText () devuelve -1 cuando falla, entonces cualquier otra llamada a m_statistics que tome uno de esos controladores debería salir correctamente si se pasa -1.

También puede colocar el error de impresión dentro de la función que realmente está fallando. En su ejemplo, createText () probablemente tenga mucha más información sobre lo que salió mal, por lo que podrá volcar un error más significativo en el registro. En este caso, hay pocos beneficios al enviar el manejo / impresión de errores a las personas que llaman. Hágalo cuando las personas que llaman necesiten personalizar el manejo (o usar la inyección de dependencia). Tenga en cuenta que tener una consola en el juego que pueda aparecer cuando se registra un error es una buena idea y también ayuda aquí.

La mejor opción (ya presentada en la serie vinculada de artículos anteriores) para las llamadas que no espera fallar en ningún entorno sano, como el simple acto de crear blobs de texto para un sistema de estadísticas, es simplemente tener la función que falló (createText en su ejemplo) abortar. Puede estar razonablemente seguro de que createText () no fallará en la producción a menos que algo esté totalmente olvidado (por ejemplo, el usuario eliminó los archivos de datos de fuente, o por alguna razón solo tiene 256 MB de memoria, etc.). En muchos de estos casos, ni siquiera hay algo bueno que hacer cuando ocurre una falla. ¿Sin memoria? Es posible que ni siquiera pueda hacer una asignación necesaria para crear un buen panel GUI para mostrarle al usuario el error OOM. ¿Faltan fuentes? Hace que sea difícil mostrar errores al usuario. Lo que sea que esté mal

Solo fallar es totalmente correcto siempre y cuando (a) registre el error en un archivo de registro y (b) solo lo haga en errores que no son causados ​​por acciones regulares del usuario.

No diría lo mismo en absoluto para muchas aplicaciones de servidor, donde la disponibilidad es crítica y el monitoreo de vigilancia no es suficiente, pero eso es muy diferente al desarrollo del cliente del juego . También me gustaría evitar usar C / C ++ allí, ya que las funciones de manejo de excepciones de otros lenguajes tienden a no funcionar como C ++ ya que son entornos administrados y no tienen todos los problemas de seguridad de excepciones que tiene C ++. Cualquier problema de rendimiento también se mitiga, ya que los servidores tienden a centrarse más en el paralelismo y el rendimiento que en las garantías de latencia mínima, como los clientes del juego. Incluso los servidores de juegos de acción para tiradores y similares pueden funcionar bastante bien cuando se escriben en C #, por ejemplo, ya que rara vez están llevando el hardware a sus límites, como suelen hacer los clientes de FPS.

Sean Middleditch
fuente
1
+1. Además, existe la posibilidad de agregar un atributo similar warn_unused_resulta funcionar en algunos compiladores que permite capturar código de error no manejado.
Maciej Piechotka
Vine aquí viendo C ++ en el título, y estaba totalmente seguro de que el manejo de errores monádicos ya no estaría aquí. ¡Buen material!
Adam
¿Qué pasa con los lugares donde el rendimiento no es crítico y su objetivo principal es una implementación rápida? por ejemplo cuando estás implementando la lógica del juego?
Ali1S232
¿Y qué hay de la idea de utilizar el manejo de excepciones solo para fines de depuración? como cuando lanzas el juego, esperas que todo salga bien sin problemas. por lo tanto, utiliza excepciones para buscar y corregir errores durante el desarrollo y luego, en el modo de lanzamiento, elimine todas esas excepciones.
Ali1S232
1
@Gajoo: para la lógica del juego, las excepciones solo hacen que la lógica sea más difícil de seguir (ya que hace que todo el código sea más difícil de seguir). Incluso en Pythnn y C #, las excepciones son raras para la lógica del juego, en mi experiencia. para la depuración, la afirmación difícil suele ser mucho más conveniente. Rompa y pare en el momento exacto en que algo sale mal, no después de desenrollar grandes porciones de la pila y perder todo tipo de información contextual debido a la semántica de lanzamiento de excepciones. Para la lógica de depuración, desea crear herramientas e información / editar GUI, para que los diseñadores puedan inspeccionar y ajustar.
Sean Middleditch
13

La vieja sabiduría del propio Donald Knuth es:

"Deberíamos olvidarnos de las pequeñas ineficiencias, digamos alrededor del 97% del tiempo: la optimización prematura es la raíz de todo mal".

Imprima esto como un gran póster y cuélguelo en cualquier lugar donde realice una programación seria.

Entonces, incluso si try / catch es un poco más lento, lo usaría por defecto:

  • El código debe ser lo más legible y comprensible posible. Usted puede escribir el código una vez, sino que va a leerlo muchas veces más para depurar el código, para la depuración de otro código, para entender, para la mejora, ...

  • Corrección sobre el rendimiento. Hacer las cosas if / else correctamente no es trivial. En su ejemplo, se hace incorrectamente, porque no se puede mostrar un solo error, sino varios. Tienes que usar en cascada if / then / else.

  • Más fácil de usar de manera consistente: Try / catch adopta un estilo en el que no necesita verificar errores en cada línea. Por otro lado: falta uno si / de lo contrario y su código podría causar estragos. Por supuesto solo en circunstancias irreproducibles.

Entonces el último punto:

Escuché que las excepciones son un poco más lentas que ifs

Escuché eso hace unos 15 años, no he escuchado eso de ninguna fuente creíble recientemente. Los compiladores pueden haber mejorado o lo que sea.

El punto principal es: no hacer una optimización prematura. Hágalo solo cuando pueda probar por referencia que el código en cuestión es el estrecho bucle interno de algún código muy utilizado y que el estilo de conmutación mejora considerablemente el rendimiento . Este es el caso del 3%.

AH
fuente
22
Voy a -1 esto hasta el infinito. No puedo entender el daño que esta cita de Knuth ha hecho a los cerebros de los desarrolladores. Considerando si usar excepciones no es una optimización prematura, es una decisión de diseño, una decisión de diseño importante que tiene ramificaciones mucho más allá del rendimiento.
sam hocevar
3
@SamHocevar: Lo siento, no entiendo su punto: la pregunta es sobre el posible impacto en el rendimiento cuando se usan excepciones. Mi punto: no lo pienses, el golpe no es tan malo (si lo hay) y otras cosas son mucho más importantes. Aquí parece estar de acuerdo al agregar "decisión de diseño" a mi lista. OKAY. Por otro lado, usted dice que la cita de Knuth es mala, lo que implica que la optimización prematura no es mala. Pero esto es exactamente lo que sucedió aquí. OMI: La Q no piensa en arquitectura, diseño o algoritmos diferentes, solo en excepciones y su impacto en el rendimiento.
AH
2
No estaría de acuerdo con la claridad en C ++. Los C ++ tienen excepciones no verificadas, mientras que algunos compiladores implementan valores de retorno verificados. Como resultado, tiene un código que parece más claro pero puede tener una pérdida de memoria oculta o incluso poner el código en un estado indefinido. Además, el código de terceros podría no ser seguro para excepciones. (La situación es diferente en Java / C # que ha marcado excepciones, GC, ...). A partir de la decisión de diseño: si no cruzan los puntos API, la refactorización de / a cada estilo se puede hacer semiautomáticamente con perl one-liners.
Maciej Piechotka
1
@SamHocevar: " La pregunta es sobre qué es preferible, no qué es más rápido " . Vuelva a leer el último párrafo de la pregunta. La única razón por la que incluso está pensando en no usar excepciones es porque cree que podrían ser más lentas. Ahora, si crees que hay otras preocupaciones que el OP no tuvo en cuenta, no dudes en publicarlas o votar a quienes sí lo hicieron. Pero el OP está claramente enfocado en el desempeño de las excepciones.
Nicol Bolas
3
@AH: si vas a citar a Knuth, no olvides el resto (y la OMI, la parte más importante): " Sin embargo, no debemos dejar pasar nuestras oportunidades en ese crítico 3%. Un buen programador no será Llegado a la complacencia por tal razonamiento, será prudente mirar cuidadosamente el código crítico; pero solo después de que ese código haya sido identificado ". Con demasiada frecuencia, se ve esto como una excusa para no optimizar en absoluto, o para tratar de justificar que el código sea lento en los casos en que el rendimiento no es una optimización, sino que es un requisito fundamental.
Maximus Minimus
10

Ya ha habido una excelente respuesta a esta pregunta, pero me gustaría agregar algunas ideas sobre la legibilidad del código y la recuperación de errores en su caso particular.

Creo que su código podría verse así:

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 );

Sin excepciones, sin ifs. El informe de errores se puede hacer en createText. Y createTextpuede devolver una ID de textura predeterminada que no requiere que verifique el valor de retorno, de modo que el resto del código funcione igual de bien.

sam hocevar
fuente