usando - factorial c++ do while
¿Cuáles son algunas mejores maneras de evitar el hacer mientras que(0); hackear en C++? (27)
Cuando el flujo de código es así:
if(check())
{
...
...
if(check())
{
...
...
if(check())
{
...
...
}
}
}
Generalmente he visto este trabajo para evitar el flujo de código desordenado anterior:
do {
if(!check()) break;
...
...
if(!check()) break;
...
...
if(!check()) break;
...
...
} while(0);
¿Cuáles son algunas de las mejores maneras de evitar esta solución / piratería para que se convierta en un código de nivel superior (nivel industrial)?
Cualquier sugerencia que esté fuera de la caja son bienvenidas!
Intente extraer el código en una función separada (o quizás más de una). Luego volverá de la función si la comprobación falla.
Si está demasiado unido al código circundante para hacer eso, y no puede encontrar una manera de reducir el acoplamiento, mire el código después de este bloque. Presumiblemente, limpia algunos recursos utilizados por la función. Intente administrar estos recursos utilizando un objeto RAII ; luego reemplaza cada
break
conreturn
(othrow
, si es más apropiado) y deja que el destructor del objeto limpie por ti.Si el flujo del programa es (necesariamente) tan ondulado que realmente necesitas un
goto
, entoncesgoto
lugar de darle un extraño disfraz.Si tiene reglas de codificación que prohíben ciegamente el
goto
, y realmente no puede simplificar el flujo del programa, entonces probablemente tendrá que disfrazarlo con su pirata informático.
¿Qué tal si movemos las declaraciones if a una función adicional que produzca un resultado numérico o enumeración?
int ConditionCode (void) {
if (condition1)
return 1;
if (condition2)
return 2;
...
return 0;
}
void MyFunc (void) {
switch (ConditionCode ()) {
case 1:
...
break;
case 2:
...
break;
...
default:
...
break;
}
}
Algo como esto quizás
#define EVER ;;
for(EVER)
{
if(!check()) break;
}
o usar excepciones
try
{
for(;;)
if(!check()) throw 1;
}
catch()
{
}
Usando excepciones también puede pasar datos.
Así es como lo hago.
void func() {
if (!check()) return;
...
...
if (!check()) return;
...
...
if (!check()) return;
...
...
}
Ciertamente no es la respuesta, sino una respuesta (en aras de la integridad)
En lugar de :
do {
if(!check()) break;
...
...
if(!check()) break;
...
...
if(!check()) break;
...
...
} while(0);
Podrías escribir:
switch (0) {
case 0:
if(!check()) break;
...
...
if(!check()) break;
...
...
if(!check()) break;
...
...
}
Esto sigue siendo un goto disfrazado, pero al menos ya no es un bucle. Lo que significa que no tendrá que comprobar con mucho cuidado que no hay algo que continúe escondido en algún lugar del bloque.
La construcción también es lo suficientemente simple como para que puedas esperar que el compilador la optimice.
Como lo sugiere @jamesdlin, incluso puede ocultar eso detrás de una macro como
#define BLOC switch(0) case 0:
Y usalo como
BLOC {
if(!check()) break;
...
...
if(!check()) break;
...
...
if(!check()) break;
...
...
}
Esto es posible porque la sintaxis del lenguaje C espera una declaración después de un cambio, no un bloque entre corchetes y puede poner una etiqueta de caso antes de esa declaración. Hasta ahora no vi el punto de permitir eso, pero en este caso particular es útil ocultar el interruptor detrás de una macro agradable.
El flujo de código en sí ya es un olor de código que está sucediendo mucho en la función. Si no hay una solución directa a eso (la función es una función de verificación general), entonces usar RAII para que pueda regresar en lugar de saltar a una sección final de la función podría ser mejor.
Este es un problema bien conocido y resuelto desde una perspectiva de programación funcional, tal vez la mónada.
En respuesta al comentario que recibí a continuación, he editado mi introducción aquí: puede encontrar detalles completos sobre la implementación de mónadas C++ en varios lugares que le permitirán lograr lo que sugiere Rotsor. Se necesita un tiempo para asimilar las mónadas, así que, en lugar de eso, voy a sugerir aquí un rápido mecanismo tipo mónada "pobre" para el que solo necesita saber algo más que boost :: opcional.
Configure sus pasos de cálculo de la siguiente manera:
boost::optional<EnabledContext> enabled(boost::optional<Context> context);
boost::optional<EnergisedContext> energised(boost::optional<EnabledContext> context);
Obviamente, cada paso computacional puede hacer algo como retorno boost::none
si el opcional que se le dio está vacío. Así por ejemplo:
struct Context { std::string coordinates_filename; /* ... */ };
struct EnabledContext { int x; int y; int z; /* ... */ };
boost::optional<EnabledContext> enabled(boost::optional<Context> c) {
if (!c) return boost::none; // this line becomes implicit if going the whole hog with monads
if (!exists((*c).coordinates_filename)) return boost::none; // return none when any error is encountered.
EnabledContext ec;
std::ifstream file_in((*c).coordinates_filename.c_str());
file_in >> ec.x >> ec.y >> ec.z;
return boost::optional<EnabledContext>(ec); // All ok. Return non-empty value.
}
Luego encadéalos juntos:
Context context("planet_surface.txt", ...); // Close over all needed bits and pieces
boost::optional<EnergisedContext> result(energised(enabled(context)));
if (result) { // A single level "if" statement
// do work on *result
} else {
// error
}
Lo bueno de esto es que puede escribir pruebas unitarias claramente definidas para cada paso computacional. Además, la invocación se lee como en inglés simple (como suele ser el caso con el estilo funcional).
Si no te importa la inmutabilidad y es más conveniente devolver el mismo objeto cada vez que puedas encontrar alguna variación usando shared_ptr o similar.
Estoy agregando una respuesta por el bien de la integridad. Otras respuestas indicaron que el bloque de condición grande podría dividirse en una función separada. Pero como también se señaló varias veces, este enfoque separa el código condicional del contexto original. Esta es una de las razones por las que se agregaron lambdas al lenguaje en C ++ 11. El uso de lambdas fue sugerido por otros, pero no se proporcionó una muestra explícita. He puesto uno en esta respuesta. Lo que me sorprende es que se siente muy similar al enfoque do { } while(0)
de muchas maneras, y tal vez eso signifique que todavía es un goto
disfrazado ...
earlier operations
...
[&]()->void {
if (!check()) return;
...
...
if (!check()) return;
...
...
if (!check()) return;
...
...
}();
later operations
Hay ocasiones en que usar goto
es en realidad la respuesta CORRECTA, al menos para aquellos que no se han criado en la creencia religiosa de que " goto
nunca puede ser la respuesta, sin importar cuál sea la pregunta", y este es uno de esos casos.
Este código está usando el hack de do { ... } while(0);
con el único propósito de vestir un goto
como un break
. Si vas a usar goto
, entonces sé abierto al respecto. No tiene sentido que el código sea MÁS DIFÍCIL para leer.
Una situación particular es cuando tienes un montón de código con condiciones bastante complejas:
void func()
{
setup of lots of stuff
...
if (condition)
{
...
...
if (!other condition)
{
...
if (another condition)
{
...
if (yet another condition)
{
...
if (...)
...
}
}
}
....
}
finish up.
}
En realidad, puede hacer que sea CLARO que el código sea correcto al no tener una lógica tan compleja.
void func()
{
setup of lots of stuff
...
if (!condition)
{
goto finish;
}
...
...
if (other condition)
{
goto finish;
}
...
if (!another condition)
{
goto finish;
}
...
if (!yet another condition)
{
goto finish;
}
...
....
if (...)
... // No need to use goto here.
finish:
finish up.
}
Edición: para aclarar, de ninguna manera estoy proponiendo el uso de goto
como una solución general. Pero hay casos en los que goto
es una mejor solución que otras soluciones.
Por ejemplo, imagine que estamos recopilando algunos datos, y las diferentes condiciones que se están probando son una especie de "este es el final de los datos que se recopilan", lo que depende de algún tipo de marcadores de "continuar / finalizar" que varían según el lugar estás en el flujo de datos
Ahora, cuando hayamos terminado, debemos guardar los datos en un archivo.
Y sí, a menudo hay otras soluciones que pueden proporcionar una solución razonable, pero no siempre.
No estoy particularmente en la forma de usar break
o return
en tal caso. Dado que normalmente cuando nos enfrentamos a una situación así, suele ser un método relativamente largo.
Si tenemos varios puntos de salida, puede causar dificultades cuando queremos saber qué es lo que hará que se ejecute cierta lógica. situación:
Por ejemplo,
if (conditionA) {
....
if (conditionB) {
....
if (conditionC) {
myLogic();
}
}
}
Al observar los bloques myLogic()
, es fácil descubrir que myLogic()
solo ocurre cuando la conditionA and conditionB and conditionC
myLogic()
conditionA and conditionB and conditionC
myLogic()
son verdaderas.
Se vuelve mucho menos visible cuando hay rendimientos tempranos:
if (conditionA) {
....
if (!conditionB) {
return;
}
if (!conditionD) {
return;
}
if (conditionC) {
myLogic();
}
}
Ya no podemos navegar hacia arriba desde myLogic()
, mirando el bloque adjunto para descubrir la condición.
Hay diferentes soluciones que he usado. Aqui esta uno de ellos:
if (conditionA) {
isA = true;
....
}
if (isA && conditionB) {
isB = true;
...
}
if (isB && conditionC) {
isC = true;
myLogic();
}
(Por supuesto, es bienvenido usar la misma variable para reemplazar a todos isA isB isC
).
Tal enfoque al menos le dará al lector de código, que myLogic()
se ejecuta cuando isB && conditionC
. Al lector se le da una pista de que necesita buscar más a fondo lo que hará que isB sea cierto.
Para mí do{...}while(0)
está bien. Si no desea ver el do{...}while(0)
, puede definir palabras clave alternativas para ellos.
Ejemplo:
//--------SomeUtilities.hpp---------
#define BEGIN_TEST do{
#define END_TEST }while(0);
//--------SomeSourceFile.cpp--------
BEGIN_TEST
if(!condition1) break;
if(!condition2) break;
if(!condition3) break;
if(!condition4) break;
if(!condition5) break;
//processing code here
END_TEST
Creo que el compilador eliminará la condición innecesaria while(0)
en do{...}while(0)
en versión binaria y convertirá los saltos en salto incondicional. Puede verificar su versión en lenguaje ensamblador para estar seguro.
El uso de goto
también produce un código más limpio y es sencillo con la lógica de condición y salto. Puedes hacer lo siguiente:
{
if(!condition1) goto end_blahblah;
if(!condition2) goto end_blahblah;
if(!condition3) goto end_blahblah;
if(!condition4) goto end_blahblah;
if(!condition5) goto end_blahblah;
//processing code here
}end_blah_blah:; //use appropriate label here to describe...
// ...the whole code inside the block.
Tenga en cuenta que la etiqueta se coloca después del cierre }
. Este es el evitar un posible problema en goto
que accidentalmente está colocando un código entre porque no vio la etiqueta. Ahora es como do{...}while(0)
sin código de condición.
Para hacer este código más limpio y más comprensible, puedes hacer esto:
//--------SomeUtilities.hpp---------
#define BEGIN_TEST {
#define END_TEST(_test_label_) }_test_label_:;
#define FAILED(_test_label_) goto _test_label_
//--------SomeSourceFile.cpp--------
BEGIN_TEST
if(!condition1) FAILED(NormalizeData);
if(!condition2) FAILED(NormalizeData);
if(!condition3) FAILED(NormalizeData);
if(!condition4) FAILED(NormalizeData);
if(!condition5) FAILED(NormalizeData);
END_TEST(NormalizeData)
Con esto, puede hacer bloques anidados y especificar dónde desea salir / saltar.
//--------SomeUtilities.hpp---------
#define BEGIN_TEST {
#define END_TEST(_test_label_) }_test_label_:;
#define FAILED(_test_label_) goto _test_label_
//--------SomeSourceFile.cpp--------
BEGIN_TEST
if(!condition1) FAILED(NormalizeData);
if(!condition2) FAILED(NormalizeData);
BEGIN_TEST
if(!conditionAA) FAILED(DecryptBlah);
if(!conditionBB) FAILED(NormalizeData); //Jump out to the outmost block
if(!conditionCC) FAILED(DecryptBlah);
// --We can now decrypt and do other stuffs.
END_TEST(DecryptBlah)
if(!condition3) FAILED(NormalizeData);
if(!condition4) FAILED(NormalizeData);
// --other code here
BEGIN_TEST
if(!conditionA) FAILED(TrimSpaces);
if(!conditionB) FAILED(TrimSpaces);
if(!conditionC) FAILED(NormalizeData); //Jump out to the outmost block
if(!conditionD) FAILED(TrimSpaces);
// --We can now trim completely or do other stuffs.
END_TEST(TrimSpaces)
// --Other code here...
if(!condition5) FAILED(NormalizeData);
//Ok, we got here. We can now process what we need to process.
END_TEST(NormalizeData)
El código de Spaghetti no es culpa de goto
, es culpa del programador. Aún puedes producir código espagueti sin usar goto
.
Puedes usar un patrón de continuación simple con una variable bool
:
bool goOn;
if ((goOn = check0())) {
...
}
if (goOn && (goOn = check1())) {
...
}
if (goOn && (goOn = check2())) {
...
}
if (goOn && (goOn = check3())) {
...
}
Esta cadena de ejecución se detendrá tan pronto como checkN
devuelva un false
. No se realizarán más check...()
llamadas se realizarían debido a un cortocircuito del operador de &&
. Además, la optimización de los compiladores es lo suficientemente inteligente como para reconocer que establecer goOn
en false
es una calle de una sola vía, e inserta el goto end
falta. Como resultado, el rendimiento del código anterior sería idéntico al de un do
/ while(0)
, solo que sin un doloroso golpe a su legibilidad.
Recomendaría un enfoque similar a la respuesta de Mats menos el goto
innecesario. Solo pon la lógica condicional en la función. Cualquier código que se ejecute siempre debe ir antes o después de invocar la función en el llamador:
void main()
{
//do stuff always
func();
//do other stuff always
}
void func()
{
if (!condition)
return;
...
if (!other condition)
return;
...
if (!another condition)
return;
...
if (!yet another condition)
return;
...
}
Se considera una práctica aceptable aislar estas decisiones en una función y usar return
s en lugar de break
s. Si bien todas estas comprobaciones corresponden al mismo nivel de abstracción que la función, es un enfoque bastante lógico.
Por ejemplo:
void foo(...)
{
if (!condition)
{
return;
}
...
if (!other condition)
{
return;
}
...
if (!another condition)
{
return;
}
...
if (!yet another condition)
{
return;
}
...
// Some unconditional stuff
}
Si no necesita introducir variables locales durante la ejecución, a menudo puede aplanar esto:
if (check()) {
doStuff();
}
if (stillOk()) {
doMoreStuff();
}
if (amIStillReallyOk()) {
doEvenMore();
}
// edit
doThingsAtEndAndReportErrorStatus()
Similar a la respuesta de dasblinkenlight, pero evita la asignación dentro de la if
un revisor de código podría "arreglarlo":
bool goOn = check0();
if (goOn) {
...
goOn = check1();
}
if (goOn) {
...
goOn = check2();
}
if (goOn) {
...
}
...
Utilizo este patrón cuando es necesario verificar los resultados de un paso antes del siguiente, lo que difiere de una situación en la que todos los chequeos podrían hacerse por adelantado con un gran tipo if( check1() && check2()...
.
Utilizar excepciones. Su código se verá mucho más limpio (y las excepciones se crearon exactamente para manejar los errores en el flujo de ejecución de un programa). Para limpiar recursos (descriptores de archivos, conexiones de base de datos, etc.), lea el artículo ¿Por qué C ++ no proporciona una construcción "finalmente"? .
#include <iostream>
#include <stdexcept> // For exception, runtime_error, out_of_range
int main () {
try {
if (!condition)
throw std::runtime_error("nope.");
...
if (!other condition)
throw std::runtime_error("nope again.");
...
if (!another condition)
throw std::runtime_error("told you.");
...
if (!yet another condition)
throw std::runtime_error("OK, just forget it...");
}
catch (std::runtime_error &e) {
std::cout << e.what() << std::endl;
}
catch (...) {
std::cout << "Caught an unknown exception/n";
}
return 0;
}
¿Por qué no se respondió el método de marcado que se utiliza desde las edades.
//you can use something like this (pseudocode)
long var = 0;
if(condition) flag a bit in var
if(condition) flag another bit in var
if(condition) flag another bit in var
............
if(var == certain number) {
Do the required task
}
TLDR : RAII , código transaccional (solo establece resultados o devuelve datos cuando ya está computado) y excepciones.
Respuesta larga:
En C , la mejor práctica para este tipo de código es agregar una etiqueta SALIR / LIMPIAR / otra en el código, donde se realiza la limpieza de los recursos locales y se devuelve un código de error (si lo hubiera). Esta es la mejor práctica porque divide el código naturalmente en inicialización, cálculo, confirmación y devolución:
error_code_type c_to_refactor(result_type *r)
{
error_code_type result = error_ok; //error_code_type/error_ok defd. elsewhere
some_resource r1, r2; // , ...;
if(error_ok != (result = computation1(&r1))) // Allocates local resources
goto cleanup;
if(error_ok != (result = computation2(&r2))) // Allocates local resources
goto cleanup;
// ...
// Commit code: all operations succeeded
*r = computed_value_n;
cleanup:
free_resource1(r1);
free_resource2(r2);
return result;
}
En C, en la mayoría de las bases de código, el if(error_ok != ...
y goto
code usualmente está oculto detrás de algunas macros convenientes ( RET(computation_result)
, ENSURE_SUCCESS(computation_result, return_code)
, etc.).
C ++ ofrece herramientas adicionales sobre C :
La funcionalidad del bloque de limpieza se puede implementar como RAII, lo que significa que ya no necesita el bloque de
cleanup
completo y permite que el código del cliente agregue declaraciones de devolución anticipada.Usted lanza cuando no puede continuar, transformando todo el
if(error_ok != ...
en llamadas directas).
Código de C ++ equivalente:
result_type cpp_code()
{
raii_resource1 r1 = computation1();
raii_resource2 r2 = computation2();
// ...
return computed_value_n;
}
Esta es la mejor práctica porque:
Es explícito (es decir, mientras que el manejo de errores no es explícito, el flujo principal del algoritmo es)
Es sencillo escribir código de cliente
Es minimo
Es simple
No tiene construcciones de código repetitivas.
No utiliza macros.
No usa we
do { ... } while(0)
construyeEs reutilizable con un esfuerzo mínimo (es decir, si quiero copiar la llamada a
computation2();
a una función diferente, no tengo que asegurarme de agregar undo { ... } while(0)
en el código nuevo, ni#define
una macro de envoltura goto y una etiqueta de limpieza, ni ninguna otra cosa).
Otro patrón útil si necesita diferentes pasos de limpieza dependiendo de dónde está la falla:
private ResultCode DoEverything()
{
ResultCode processResult = ResultCode.FAILURE;
if (DoStep1() != ResultCode.SUCCESSFUL)
{
Step1FailureCleanup();
}
else if (DoStep2() != ResultCode.SUCCESSFUL)
{
Step2FailureCleanup();
processResult = ResultCode.SPECIFIC_FAILURE;
}
else if (DoStep3() != ResultCode.SUCCESSFUL)
{
Step3FailureCleanup();
}
...
else
{
processResult = ResultCode.SUCCESSFUL;
}
return processResult;
}
Si utiliza el mismo controlador de errores para todos los errores, y cada paso devuelve un error que indica que es correcto:
if(
DoSomething() &&
DoSomethingElse() &&
DoAThirdThing() )
{
// do good condition action
}
else
{
// handle error
}
(Similar a la respuesta de tyzoid, pero las condiciones son las acciones y el && evita que ocurran acciones adicionales después del primer fallo)
Consolidar en una if
declaración:
if(
condition
&& other_condition
&& another_condition
&& yet_another_condition
&& ...
) {
if (final_cond){
//Do stuff
} else {
//Do other stuff
}
}
Este es el patrón utilizado en lenguajes como Java donde se eliminó la palabra clave goto.
Me sorprende la cantidad de respuestas diferentes que se presentan aquí. Pero, finalmente, en el código que tengo que cambiar (es decir, eliminar este do-while(0)
truco o cualquier otra cosa), hice algo diferente de cualquiera de las respuestas que se mencionan aquí y estoy confundido por qué nadie pensó esto. Esto es lo que hice:
Código inicial:
do {
if(!check()) break;
...
...
if(!check()) break;
...
...
if(!check()) break;
...
...
} while(0);
finishingUpStuff.
Ahora:
finish(params)
{
...
...
}
if(!check()){
finish(params);
return;
}
...
...
if(!check()){
finish(params);
return;
}
...
...
if(!check()){
finish(params);
return;
}
...
...
Entonces, lo que se ha hecho aquí es que las cosas de acabado se han aislado en una función y, de repente, ¡se han vuelto tan simples y limpias!
Pensé que valía la pena mencionar esta solución, así que la proporcioné aquí.
No soy programador de C++ , por lo que no escribiré ningún código aquí, pero hasta ahora nadie ha mencionado una solución orientada a objetos. Así que aquí está mi conjetura sobre eso:
Tener una interfaz genérica que proporcione un método para evaluar una sola condición. Ahora puede usar una lista de implementaciones de esas condiciones en su objeto que contiene el método en cuestión. Repite la lista y evalúa cada condición, posiblemente rompiéndose antes si falla una.
Lo bueno es que dicho diseño se adhiere muy bien al principio abierto / cerrado , porque puede agregar fácilmente nuevas condiciones durante la inicialización del objeto que contiene el método en cuestión. Incluso puede agregar un segundo método a la interfaz con el método para la evaluación de la condición y devolver una descripción de la condición. Esto puede ser usado para sistemas de auto-documentación.
La desventaja, sin embargo, es que hay un poco más de sobrecarga debido al uso de más objetos y la iteración en la lista.
Primero, un breve ejemplo para mostrar por qué goto
no es una buena solución para C ++:
struct Bar {
Bar();
};
extern bool check();
void foo()
{
if (!check())
goto out;
Bar x;
out:
}
Intente compilar esto en un archivo de objeto y vea qué sucede. A continuación, intente el equivalente do
+ break
+ while(0)
.
Eso fue un aparte. El punto principal sigue.
Esos pequeños trozos de código a menudo requieren algún tipo de limpieza en caso de que falle la función completa. Esas limpiezas generalmente quieren que se realicen en el orden opuesto a los trozos en sí, ya que "desenrolla" el cálculo parcialmente terminado.
Una opción para obtener estas semánticas es RAII ; Ver la respuesta de @utnapistim. C ++ garantiza que los destructores automáticos se ejecuten en el orden opuesto a los constructores, lo que naturalmente proporciona un "desenrollado".
Pero eso requiere muchas clases de RAII. A veces, una opción más simple es simplemente usar la pila:
bool calc1()
{
if (!check())
return false;
// ... Do stuff1 here ...
if (!calc2()) {
// ... Undo stuff1 here ...
return false;
}
return true;
}
bool calc2()
{
if (!check())
return false;
// ... Do stuff2 here ...
if (!calc3()) {
// ... Undo stuff2 here ...
return false;
}
return true;
}
...y así. Esto es fácil de auditar, ya que coloca el código de "deshacer" al lado del código de "hacer". La auditoría fácil es buena. También hace que el flujo de control sea muy claro. Es un patrón útil para C, también.
Puede requerir que las calc
funciones tomen muchos argumentos, pero eso no suele ser un problema si sus clases / estructuras tienen buena cohesión. (Es decir, las cosas que pertenecen juntas viven en un solo objeto, por lo que estas funciones pueden llevar punteros o referencias a una pequeña cantidad de objetos y aún hacer un montón de trabajo útil)
Si el código tiene un bloque largo de sentencias if..else if..else, puede intentar reescribir todo el bloque con la ayuda de Functors
o function pointers
. Puede que no sea siempre la solución correcta, pero a menudo lo es.
http://www.cprogramming.com/tutorial/functors-function-objects-in-c++.html
typedef bool (*Checker)();
Checker * checkers[]={
&checker0,&checker1,.....,&checkerN,NULL
};
bool checker1(){
if(condition){
.....
.....
return true;
}
return false;
}
bool checker2(){
if(condition){
.....
.....
return true;
}
return false;
}
......
void doCheck(){
Checker ** checker = checkers;
while( *checker && (*checker)())
checker++;
}
¿Qué hay sobre eso?