2012-01-03 17 views
13

considerar esta clase simple que demuestra RAII en C++ (Desde la parte superior de la cabeza):¿Cuándo tiene RAII una ventaja sobre GC?

class X { 
public: 
    X() { 
     fp = fopen("whatever", "r"); 
     if (fp == NULL) 
     throw some_exception(); 
    } 

    ~X() { 
     if (fclose(fp) != 0){ 
      // An error. Now what? 
     } 
    } 
private: 
    FILE *fp; 
    X(X const&) = delete; 
    X(X&&) = delete; 
    X& operator=(X const&) = delete; 
    X& operator=(X&&) = delete; 
} 

No puedo lanzar una excepción en el destructor. Estoy teniendo un error, pero no hay manera de informarlo. Y este ejemplo es bastante genérico: puedo hacerlo no solo con archivos, sino también con, por ejemplo, subprocesos, recursos gráficos, ... Observo cómo, p. la página wikipedia RAII barre todo el tema debajo de la alfombra: http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization

Me parece que RAII solo es útil si se garantiza que la destrucción ocurrirá sin error. Los únicos recursos que conozco con esta propiedad es la memoria. Ahora me parece que, por ejemplo, Boehm descarta de manera convincente la idea de que la gestión manual de la memoria es una buena idea en cualquier situación común, entonces, ¿dónde está la ventaja en la forma C++ de usar RAII?

Sí, lo sé GC es un hereje poco en el mundo de C++ ;-)

+6

¿No tiene el mismo problema en una sección 'finally' en, por ejemplo, Java? –

+1

uno de los ejemplos que viene (no es muy relevante para su ejemplo, pero ...) son guardias de bloqueo: http://www.boost.org/doc/libs/1_48_0/doc/html/thread/synchronization.html# thread.synchronization.locks.lock_guard – Anycorn

+5

Al hacer un uso incorrecto de los recolectores de basura para la administración general de recursos (como los identificadores de archivos), ocurre exactamente el mismo problema. ¿Dónde debería el recolector de basura arrojar excepciones? El código interesado ya pasó. – thiton

Respuesta

14

RAII, a diferencia de GC, es determinista. Sabrá exactamente cuándo se lanzará un recurso, en lugar de "en algún momento en el futuro se lanzará", dependiendo de cuándo el GC decide que necesita volver a ejecutarse.

Ahora, veamos el problema real que parece tener. Esta discusión surgió en la sala de chat < C++ > hace un momento acerca de lo que debe hacer si el destructor de un objeto RAII puede fallar.

La conclusión fue que la mejor forma sería la de proporcionar una close(), destroy() o función miembro similares específica que es llamada por el destructor pero también puede ser llamado antes de eso, si quiere eludir la "excepción durante la pila relajarse" problema. Luego establecería un indicador que evitaría que se llamara en el destructor. std::(i|o)fstream, por ejemplo, hace exactamente eso: cierra el archivo en su destructor, pero también proporciona un método close().

+0

Esta parece ser la respuesta más realista a mi pregunta hasta el momento. En aplicaciones RT, estaría de acuerdo contigo. Sin embargo, la mayoría de las aplicaciones estándar pueden vivir perfectamente con una desaceleración de unos pocos milisegundos de vez en cuando. – hyperman

+5

@ user844382: ¿qué milisegundos? La demora antes de que un objeto sea GCed podría ser de horas, si la aplicación no está asignando recursos. No hay "ralentización": con GC mark-sweep la aplicación continúa ejecutándose, y un finalizador puede o no ejecutarse algún tiempo después. –

+0

@user: Exactamente lo que dice Steve. Si no hay necesidad de liberar el espacio de recursos, el GC ni siquiera considerará liberar su recurso (excepto si explícitamente así lo indica 'GC.Collect()' por ejemplo). – Xeo

13

Este es un argumento del hombre de paja, porque no estamos hablando de recolección de basura (cancelación de asignación de memoria), que está hablando gestión de recursos generales.

Si hizo un mal uso de un recolector de basura para cerrar archivos de esta manera, entonces tendría la misma situación: tampoco podría lanzar una excepción. Las mismas opciones estarían abiertas para usted: ignorar el error o, mucho mejor, iniciar sesión.

+1

Tiene razón cuando dice que GC no resuelve el caso que no es de memoria tampoco, y mi pregunta podría redactarse mejor. Tratando de reformularlo un poco mejor: RAII no es mejor en el manejo de recursos que GC, y requiere que se escriba (y especialmente: depurar) una gran cantidad de código. Entonces, ¿en qué caso serías mejor que RAII? – hyperman

+0

Puede ser el argumento de B.Stroustup sobre por qué GC aún no está implementado en el estándar responderá parcialmente a la pregunta - ver "Evolución de un lenguaje en y para el mundo real: C++ 1991-2006", cap. 5.4 "Recolección automática de basura" (http://www2.research.att.com/~bs/hopl-almost-final.pdf) – SChepurin

7

El mismo problema ocurre en la recolección de basura.

Sin embargo, vale la pena señalar que si no hay ningún error en su código ni en el código de la biblioteca que alimenta su código, la eliminación de un recurso será nunca. delete nunca falla a menos que haya dañado su pila. Esta es la misma historia para cada recurso. La falla en la destrucción de un recurso es una falla que termina la aplicación, no una agradable excepción de "manejame".

+0

Entonces, ¿sería aceptable imprimir un error y salir? ¿O mejor ignóralo (posiblemente después de iniciar sesión)? –

+2

Su segundo párrafo está un poco por encima de la IMO, ya que puedo imaginar muchas razones por las que la desasignación de recursos generales podría fallar que no son escenarios de fin de mundo. Si un disco se llena y no puede cerrar un archivo, se lo notifica al usuario y vuelve a intentarlo más tarde. –

+0

No creo que esto sea siempre cierto. Por ejemplo, si no se borra un archivo temporal, no necesariamente se debe finalizar la aplicación. – StackedCrooked

2

P. ¿Cuándo tiene RAII una ventaja sobre GC?

A. En todos los casos donde los errores de destrucción no son interesantes (es decir, no tiene una forma efectiva de manejarlos de todos modos).

Tenga en cuenta que incluso con la recolección de basura, que tendría que ejecutar el 'dispuestos' (cierre, la liberación de lo que sea) la acción de forma manual, por lo que sólo puede mejorar el patrón RIIA de la misma manera:

class X{ 
    FILE *fp; 
    X(){ 
     fp=fopen("whatever","r"); 
     if(fp==NULL) throw some_exception(); 
    } 

    void close() 
    { 
     if (!fp) 
      return; 
     if(fclose(fp)!=0){ 
      throw some_exception(); 
     } 
     fp = 0; 
    } 

    ~X(){ 
     if (fp) 
     { 
      if(fclose(fp)!=0){ 
       //An error. You're screwed, just throw or std::terminate 
      } 
     } 
    } 
} 
+0

Esto es casi lo que Iba a publicar: agregue un método flush(). Si falla la ventana del destructor, no, no estás jodido. El archivo está cerrado, pero es posible que algunos datos no se hayan guardado. Si la persona que llama quería una excepción para eso, la persona que llama debería haber llamado a flush() primero. – hvd

+1

Tu primera oración no es una oración. –

+0

@hvd: bueno, quise decir 'estás jodido' _en general_ ya que es claramente la intención del OP. Para este destructor en particular, sí, tienes razón. – sehe

4

Primero: no se puede hacer nada realmente útil con el error si el objeto de archivo está GCed y no cierra el ARCHIVO *. Entonces los dos son equivalentes en la medida de lo posible.

En segundo lugar, el patrón de "correcta" es la siguiente:

class X{ 
    FILE *fp; 
    public: 
    X(){ 
     fp=fopen("whatever","r"); 
     if(fp==NULL) throw some_exception(); 
    } 
    ~X(){ 
     try { 
      close(); 
     } catch (const FileError &) { 
      // perhaps log, or do nothing 
     } 
    } 
    void close() { 
     if (fp != 0) { 
      if(fclose(fp)!=0){ 
       // may need to handle EAGAIN and EINTR, otherwise 
       throw FileError(); 
      } 
      fp = 0; 
     } 
    } 
}; 

Uso:

X x; 
// do stuff involving x that might throw 
x.close(); // also might throw, but if not then the file is successfully closed 

Si "hacer cosas" tiros, entonces es más o menos no importa si el identificador de archivo está cerrado con éxito o no. La operación ha fallado, por lo que el archivo es normalmente inútil de todos modos. Alguien más arriba en la cadena de llamadas puede saber qué hacer al respecto, dependiendo de cómo se use el archivo, quizás debería eliminarse, quizás dejarse solo en su estado parcialmente escrito. Independientemente de lo que hagan, deben ser conscientes de que, además del error descrito por la excepción que ven, es posible que el búfer de archivo no se haya descargado.

RAII se utiliza aquí para recursos de gestión. El archivo se cierra sin importar nada. Pero RAII no se usa para detectando si una operación ha tenido éxito - si desea hacerlo, llame al x.close(). El GC tampoco se usa para detectar si una operación ha tenido éxito, por lo que las dos son iguales en ese conteo.

Se produce una situación similar cada vez que usa RAII en un contexto donde está definiendo algún tipo de transacción: RAII puede retrotraer una transacción abierta en una excepción, pero suponiendo que todo va bien, el programador debe transacción.

La respuesta a su pregunta: la ventaja de RAII, y la razón por la que termina descargando o cerrando objetos de archivo en finally cláusulas en Java, es que a veces desea que se limpie el recurso (en la medida de lo posible) ser) inmediatamente al salir del alcance, para que el próximo bit de código sepa que ya ha sucedido. Mark-sweep GC no garantiza eso.

+2

Como una solución práctica, estoy de acuerdo. Sin embargo, en realidad estás probando mi punto: el destructor aquí no ayuda al programador.Sí, el archivo está cerrado o se destruyó el mutex, pero cuando algo falla, tiene una corrupción de archivos silenciosa, un punto muerto extraño, etc. Prefiero que mi aplicación se bloquee en este punto. – hyperman

+0

@ user844382: pensé que estabas haciendo una pregunta: tu blog es para hacer puntos. El destructor ayuda al programador, asegura que se llame a 'fclose' en' FILE * '. No hace todo lo demás que el programador quiera hacer. Si tu punto era que GC hace todo (incluso si hace todo lo que RAII hace), entonces tu punto es simplemente incorrecto. Si realmente desea que la aplicación se bloquee, puede llamar a 'abort' o eliminar la referencia de un puntero nulo en lugar de iniciar sesión en el punto que indico. –

+1

Ah, y la corrupción del archivo es solo "silenciosa" si asume incorrectamente que el archivo se ha escrito sin llamar con éxito a 'cerrar'. Así que no hagas eso, más de lo que supondrías que el archivo está escrito sin llamar primero a 'fwrite' para realmente escribir algunos datos. En cuanto a los mutexes, no sé en qué circunstancias está fallando la liberación de los mutex, pero sugiero que se arregle el código circundante, ya que los únicos fallos documentados de (por ejemplo) 'pthread_mutex_unlock' son que la entrada no es un mutex o isn válido. propiedad de la persona que llama. –

4

Las excepciones en los destructores nunca son útiles por una simple razón: los destruidores destruyen objetos que el código de ejecución ya no necesita. Cualquier error que ocurra durante su desasignación se puede manejar de forma segura de manera independiente del contexto, como iniciar sesión, mostrar al usuario, ignorar o llamar al std::terminate. Al código circundante no le importa porque ya no necesita el objeto. Por lo tanto, no es necesario propagar una excepción a través de la pila y cancelar el cálculo actual.

En su ejemplo, fp podría insertarse con seguridad en una cola global de archivos no cerrables y manejarse más tarde. El código de llamada puede continuar sin problemas.

Según este argumento, los destructores muy rara vez tienen que lanzar. En la práctica, realmente rara vez lanzan, lo que explica el uso generalizado de RAII.

+0

El propósito de un destructor no es destruir cosas que ya no se necesitan. El objetivo es hacer que cualquier recurso que esté usando un objeto (memoria, archivos, identificadores GDI o lo que sea) esté disponible para su uso futuro por otros objetos. Si una rutina tiene una variable automática 'Foo' del tipo de clase' Bar', esa variable dejará de existir cuando la rutina finalice, sin que el destructor tenga que hacer nada. El propósito del destructor no es cambiar algo que va a dejar de existir, sino manipular entidades externas que deberían ser utilizables sin el objeto destruido. – supercat

4

Quiero incluir algunas ideas más sobre "RAII" vs. GC. Los aspectos de usar algún tipo de cierre, destrucción, finalización, cualquiera que sea la función ya se explican como es el aspecto de la liberación determinística de recursos.Hay, al menos, dos instalaciones más importantes que se habilitan mediante el uso de destructores y, por lo tanto, hacer el seguimiento de los recursos de una manera controlada programador:

  1. En el mundo RAII que es posible tener un puntero rancio, es decir, un puntero que apunta a un objeto ya destruido. Lo que parece ser una cosa mala realmente permite que los objetos relacionados se ubiquen muy cerca en la memoria. Incluso si no encajan en la misma línea de caché, al menos encajarían en la página de memoria. En cierta medida, también podría lograrse una mayor proximidad compactando el recolector de basura, pero en el mundo de C++ esto se produce naturalmente y ya está determinado en tiempo de compilación.
  2. Aunque normalmente la memoria se acaba de asignar y liberar utilizando los operadores new y delete es posible asignar memoria, p. Ej. de una agrupación y arregle para un uso de memoria compactador de los objetos que se sabe están relacionados. Esto también se puede usar para colocar objetos en áreas de memoria dedicadas, p. memoria compartida u otros rangos de direcciones para hardware especial.

Aunque estos usos no necesariamente usan las técnicas de RAII directamente, están habilitados por el control más explícito sobre la memoria. Dicho esto, también hay usos de memoria en los que la recolección de basura tiene una clara ventaja, p. al pasar objetos entre múltiples hilos. En un mundo ideal, ambas técnicas estarían disponibles y C++ está tomando algunas medidas para apoyar la recolección de basura (a veces denominada "recolección de basura") para enfatizar que está tratando de dar una vista de memoria infinita del sistema, es decir, los objetos recolectados no son destruido pero su ubicación de memoria se reutiliza). Las discusiones hasta ahora no siguen la ruta tomada por C++/CLI de usar dos tipos diferentes de referencias y punteros.

1

Se supone que los destructores son siempre exitosos. ¿Por qué no solo asegurarse de que fclose no fallará?

Siempre puede hacer fflush o alguna otra cosa manualmente y comprobar el error para asegurarse de que fclose tendrá éxito más adelante.

Cuestiones relacionadas