2008-11-20 11 views
11

Dado que no hay finally en C++ you have to use the RAII patrón de diseño en su lugar, si desea que su código sea excepcionalmente seguro. Una forma de hacer esto es mediante el uso del destructor de una clase local de esta manera:Reemplazo correcto de la falta 'finalmente' en C++

void foo() { 
    struct Finally { 
     ~Finally() { /* cleanup code */ } 
    } finalizer(); 
    // ...code that might throw an exception... 
} 

Esta es una gran ventaja sobre la solución recta hacia adelante, ya que no tiene que escribir el código de limpieza 2 veces:

try { 
    // ...code that might throw an exception... 
    // cleanup code (no exception) 
} catch (...) { 
    // cleanup code (exception) 
    throw; 
} 

Una gran desventaja de la solución de clase local es que no puede acceder directamente a las variables locales en su código de limpieza. Por lo que se hinchan su código mucho si es necesario tener acceso a ellos sin tener en cuenta:

void foo() { 
    Task* task; 
    while (task = nextTask()) { 
     task->status = running; 
     struct Finally { 
      Task* task; 
      Finally(Task* task) : task(task) {} 
      ~Finally() { task->status = idle; } 
     } finalizer(task); 
     // ...code that might throw an exception... 
    } 
} 

Así que mi pregunta es: ¿Hay una solución que combina ambas ventajas? Para que a) a no tenga que escribir código duplicado yb) pueda acceder a las variables locales en el código de limpieza, como task en el último ejemplo, pero sin dicho código inflado.

+0

Eso es feo. ¡Deberías crear un RunObject o algo así! –

+0

+1 por preguntar esto porque es muy común ver a personas que no están familiarizadas con 'RAII' pensando que' finalmente' es bueno ... –

+0

"no tiene que escribir el código de limpieza 2 veces". ¿Por qué escribirías código dos veces, por alguna razón? Para eso están las subrutinas = P Neutral en la pregunta de RAII vs finalmente, pero escribir código dos veces es una pista falsa: podría crear una rutina de limpieza, pasarle tareas y llamarla en dos lugares. No se necesita duplicación de código. En su ejemplo simple, eso lógicamente es parte del destructor de la tarea. Pero si hay alguna razón por la que desea la limpieza en foo, entonces cree una rutina de limpieza local allí. – ToolmakerSteve

Respuesta

15

En lugar de definir struct Finally, se podría extraer el código de limpieza en función de la clase Task y el uso de Loki ScopeGuard.

ScopeGuard guard = MakeGuard(&Task::cleanup, task); 

Ver también este DrDobb's article y esto other article para más información sobre ScopeGuards.

+0

Un detalle a tener en cuenta es que si el propio código de limpieza arroja, utilizando ScopeGuard descartará silenciosamente la excepción lanzada (que ciertamente es razonable dado el manejo de excepción durante la destrucción de C++) - el primer ejemplo de la operación terminará y el segundo arrojará la nueva excepción del código de limpieza en lugar de la excepción original, si alguno –

8

No creo que haya una manera más clara de lograr lo que intenta hacer, pero creo que el problema principal con el 'enfoque final' en su ejemplo es un separation of concerns incorrecto.

E.g. la función foo() es responsable de la consistencia del objeto Task, esto rara vez es una buena idea, los métodos de Task deberían ser responsables de establecer el estado en algo sensato.

Me doy cuenta de que a veces hay una necesidad real de finalmente, y su código es obviamente solo un ejemplo simple para mostrar un punto, pero esos casos son raros. Y un código un poco más artificial en casos raros es aceptable para mí.

Lo que estoy tratando de decir es que rara vez tendrías la necesidad de construir finalmente, y para los pocos casos en que lo hagas, yo diría que no pierdas el tiempo construyendo de una manera más agradable. Sólo le animo a utilizar finalmente más de lo que realmente debería ...

1

Como han dicho otros, la "solución" es una mejor separación de preocupaciones. En su caso, ¿por qué la variable tarea no puede encargarse de la limpieza después de ella? Si necesita realizar alguna limpieza, no debe ser un puntero, sino un objeto RAII.

void foo() { 
// Task* task; 
ScopedTask task; // Some type which internally stores a Task*, but also contains a destructor for RAII cleanup 
    while (task = nextTask()) { 
     task->status = running; 
     // ...code that might throw an exception... 
    } 
} 

punteros inteligentes pueden ser lo que necesita en este caso (impulso :: shared_ptr eliminará el puntero por defecto, pero se puede especificar funciones Deleter personalizado en lugar, que puede realizar la limpieza takss arbitraria en su lugar. Para RAII sobre los punteros , eso es por lo general lo que usted desea.

el problema no es la falta de una palabra clave, finalmente, es que se utiliza punteros primas, que no pueden aplicar RAII.

Pero por lo general, cada tipo debe saber cómo limpiar después de sí mismo.No después de cada objeto que estaba en el alcance cuando se lanzó la excepción (que es lo que finalmente hace, y lo que estabas tratando de hacer), justo después de sí mismo. Y si cada objeto hace eso, entonces no necesita la gran función de "limpiar después de cada objeto en el alcance".

+0

borró mi comentario, como mi ejemplo de socket fue meh :) creo que estoy de acuerdo, hay pocos casos en que finalmente se puede utilizar. pero sería bueno tenerlo/simularlo :) –

-2

Llegué a C++ desde Delphi, así que sé de lo que estoy hablando. ODIO finalmente! Es feo. Realmente no creo que C++ falte finalmente.

2

normalmente uso algo más parecido a esto:

class Runner { 
private: 
    Task & task; 
    State oldstate; 
public: 
    Runner (Task &t, State newstate) : task(t), oldstate(t.status); 
    { 
    task.status = newstate; 
    }; 

    ~Runner() 
    { 
    task.status = oldstate; 
    }; 
}; 

void foo() 
{ 
    Task* task; 
    while (task = nextTask()) 
    { 
    Runner r(*task, running); 
      // ...code that might throw an exception... 
    } 
}