Respuestas interesantes: aunque estoy de acuerdo con todas ellas (hasta ahora), hay posibles connotaciones para esta pregunta que hasta ahora se han ignorado por completo.
Si el ejemplo simple anterior se amplía con la asignación de recursos y luego la verificación de errores con una posible liberación de recursos resultante, el panorama podría cambiar.
Considere el enfoque ingenuo que pueden adoptar los principiantes:
int func(..some parameters...) {
res_a a = allocate_resource_a();
if (!a) {
return 1;
}
res_b b = allocate_resource_b();
if (!b) {
free_resource_a(a);
return 2;
}
res_c c = allocate_resource_c();
if (!c) {
free_resource_b(b);
free_resource_a(a);
return 3;
}
do_work();
free_resource_c(c);
free_resource_b(b);
free_resource_a(a);
return 0;
}
Lo anterior representaría una versión extrema del estilo de regresar prematuramente. Observe cómo el código se vuelve muy repetitivo y no se puede mantener con el tiempo cuando aumenta su complejidad. Hoy en día, la gente puede usar el manejo de excepciones para detectarlos.
int func(..some parameters...) {
res_a a;
res_b b;
res_c c;
try {
a = allocate_resource_a(); # throws ExceptionResA
b = allocate_resource_b(); # throws ExceptionResB
c = allocate_resource_c(); # throws ExceptionResC
do_work();
}
catch (ExceptionBase e) {
# Could use type of e here to distinguish and
# use different catch phrases here
# class ExceptionBase must be base class of ExceptionResA/B/C
if (c) free_resource_c(c);
if (b) free_resource_b(b);
if (a) free_resource_a(a);
throw e
}
return 0;
}
Philip sugirió, después de mirar el ejemplo de goto a continuación, usar un interruptor / caja sin interrupciones dentro del bloque de captura de arriba. Uno podría cambiar (typeof (e)) y luego pasar por free_resourcex()
alto las llamadas, pero esto no es trivial y necesita consideración de diseño . Y recuerde que un interruptor / caja sin interrupciones es exactamente como el goto con etiquetas encadenadas debajo ...
Como señaló Mark B, en C ++ se considera de buen estilo seguir el principio de adquisición de recursos es inicialización , RAII en resumen. La esencia del concepto es utilizar la instanciación de objetos para adquirir recursos. Los recursos se liberan automáticamente tan pronto como los objetos salen del alcance y se llaman a sus destructores. Para los recursos interdependientes, se debe tener especial cuidado para asegurar el orden correcto de desasignación y diseñar los tipos de objetos de manera que los datos requeridos estén disponibles para todos los destructores.
O en días previos a la excepción podría hacer:
int func(..some parameters...) {
res_a a = allocate_resource_a();
res_b b = allocate_resource_b();
res_c c = allocate_resource_c();
if (a && b && c) {
do_work();
}
if (c) free_resource_c(c);
if (b) free_resource_b(b);
if (a) free_resource_a(a);
return 0;
}
Pero este ejemplo demasiado simplificado tiene varios inconvenientes: se puede usar solo si los recursos asignados no dependen entre sí (por ejemplo, no se puede usar para asignar memoria, luego abrir un identificador de archivo y luego leer datos del identificador en la memoria ), y no proporciona códigos de error individuales distinguibles como valores de retorno.
Para mantener el código rápido (!), Compacto y fácilmente legible y extensible, Linus Torvalds impuso un estilo diferente para el código del kernel que se ocupa de los recursos, incluso usando el infame goto de una manera que tiene absolutamente sentido :
int func(..some parameters...) {
res_a a;
res_b b;
res_c c;
a = allocate_resource_a() || goto error_a;
b = allocate_resource_b() || goto error_b;
c = allocate_resource_c() || goto error_c;
do_work();
error_c:
free_resource_c(c);
error_b:
free_resource_b(b);
error_a:
free_resource_a(a);
return 0;
}
La esencia de la discusión sobre las listas de correo del kernel es que la mayoría de las características del lenguaje que son "preferidas" sobre la instrucción goto son gotos implícitos, tales como if / else enormes, en forma de árbol, manejadores de excepciones, declaraciones loop / break / continue, etc. Y los goto en el ejemplo anterior se consideran correctos, ya que están saltando solo una pequeña distancia, tienen etiquetas claras y liberan el código de otro desorden para realizar un seguimiento de las condiciones de error. Esta pregunta también se ha discutido aquí en stackoverflow .
Sin embargo, lo que falta en el último ejemplo es una buena forma de devolver un código de error. Estaba pensando en agregar un result_code++
después de cada free_resource_x()
llamada y devolver ese código, pero esto compensa algunas de las ganancias de velocidad del estilo de codificación anterior. Y es difícil devolver 0 en caso de éxito. Quizás soy poco imaginativo ;-)
Entonces, sí, creo que hay una gran diferencia en la cuestión de codificar devoluciones prematuras o no. Pero también creo que es evidente solo en un código más complicado que es más difícil o imposible de reestructurar y optimizar para el compilador. Que suele ser el caso una vez que entra en juego la asignación de recursos.