¿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?
Respuestas:
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.
fuente
warn_unused_result
a funcionar en algunos compiladores que permite capturar código de error no manejado.La vieja sabiduría del propio Donald Knuth es:
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é 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%.
fuente
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í:
Sin excepciones, sin ifs. El informe de errores se puede hacer en
createText
. YcreateText
puede 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.fuente