2009-02-06 14 views
24

Pasé los últimos 4 años en C#, así que estoy interesado en las mejores prácticas actuales y los patrones de diseño comunes en C++. Considere el siguiente ejemplo parcial:¿Deberían los objetos eliminarse en C++?

class World 
{ 
public: 
    void Add(Object *object); 
    void Remove(Object *object); 
    void Update(); 
} 

class Fire : Object 
{ 
public: 
    virtual void Update() 
    { 
     if(age > burnTime) 
     { 
      world.Remove(this); 
      delete this; 
     } 
    } 
} 

Aquí tenemos un mundo responsable de administrar un conjunto de objetos y actualizarlos regularmente. El fuego es un objeto que podría agregarse al mundo bajo muchas circunstancias diferentes, pero típicamente por otro objeto que ya está en el mundo. El fuego es el único objeto que sabe cuándo se ha apagado, por lo que actualmente lo elimino. El objeto que creó el fuego probablemente ya no exista o no sea relevante.

¿Es esto algo sensato o hay un mejor diseño que ayudaría a limpiar estos objetos?

Respuesta

20

El problema con esto es que en realidad está creando un acoplamiento implícito entre el objeto y la clase mundial.

Si intento llamar a Update() fuera de la clase mundial, ¿qué ocurre? Podría terminar con el objeto que se elimina, y no sé por qué. Parece que las responsabilidades están mal mezcladas. Esto causará problemas en el momento en que use la clase Fire en una situación nueva en la que no había pensado cuando escribió este código. ¿Qué ocurre si el objeto debe eliminarse de más de un lugar? Tal vez debería eliminarse tanto del mundo, el mapa actual y el inventario del jugador? La función Actualizar lo eliminará del mundo y luego eliminará el objeto, y la próxima vez que el mapa o el inventario intente acceder al objeto, sucederán cosas malas.

En general, diría que es muy poco intuitivo que una función Update() elimine el objeto que está actualizando. También diría que no es intuitivo que un objeto se elimine a sí mismo. Es más probable que el objeto tenga algún tipo de forma de disparar un evento diciendo que ha terminado de quemarse, y cualquier persona interesada puede actuar sobre eso. Por ejemplo sacándolo del mundo. Para eliminarlo, piense en términos de propiedad.

¿A quién pertenece el objeto? ¿El mundo? Eso significa que el mundo solo puede decidir cuándo muere el objeto. Eso está bien siempre y cuando la referencia del mundo al objeto vaya a durar más que otras referencias a él. ¿Crees que el objeto es en sí mismo? ¿Y eso que significa? El objeto debe ser eliminado cuando el objeto ya no existe? No tiene sentido.

Pero si no hay un solo dueño claramente definido, poner en práctica la propiedad compartida, por ejemplo utilizando un puntero inteligente aplicación de recuento de referencias, tales como boost::shared_ptr

Pero tener una función miembro en el objeto en sí, que está codificado para eliminar el objeto de una lista específica, si existe o no, y si también existe o no en cualquier otra lista, y también elimina el objeto en sí, independientemente de las referencias que exista, es una mala idea.

+1

Tuve un momento difícil para elegir una respuesta aceptada aquí. Estoy escogiendo esto porque describe los problemas que terminé teniendo (cosas distintas de las referencias de tenencia mundiales) y también sugiere shared_ptr que terminó como parte de mi solución. Gracias. –

+1

Otra razón por la que eliminar uno mismo es malo es que impide crear el objeto en la pila en lugar del montón. Además, el acoplamiento a World hace que las pruebas independientes sean imposibles. – walrii

0

Esto no significa que haya algo intrínsecamente incorrecto en eliminar un objeto, pero otro posible método sería hacer que el mundo sea responsable de eliminar objetos como parte de :: Eliminar (suponiendo que todos los objetos eliminados también se eliminaron).

6

No hay nada realmente malo en que los objetos se eliminen en C++, la mayoría de las implementaciones de conteo de referencia usarán algo similar. Sin embargo, se ve como una técnica ligeramente esotérica, y tendrá que estar muy atento a los problemas de propiedad.

1

Generalmente no es una buena idea hacer un "eliminar esto" a menos que sea necesario o que se use de una manera muy directa. En su caso, parece que World puede simplemente eliminar el objeto cuando se elimina, a menos que haya otras dependencias que no veamos (en cuyo caso su llamada de eliminación causará errores ya que no se notifican). Mantener la propiedad fuera de su objeto permite que su objeto esté mejor encapsulado y sea más estable: el objeto no se volverá inválido en algún momento simplemente porque elige eliminarse a sí mismo.

+0

Si se refiere a World :: Remove (Object * o) {delete o}, entonces eso es malo, ya que se llamó a Remove desde el objeto que está eliminando. Eso significa que volverá a un método de un abyecto que ya no existe. – KeithB

5

Prefiero un modelo de propiedad más estricto. El mundo debería poseer los objetos y ser responsable de limpiarlos. O bien tiene mundo eliminar hacer eso o tener actualización (u otra función) devolver un valor que indica que un objeto debe ser eliminado.

También estoy adivinando que en este tipo de patrón, querrá hacer una referencia de contar sus objetos para evitar terminar con punteros colgantes.

2

Esa es una implementación de recuento de referencia bastante común y lo he usado con éxito anteriormente.

Sin embargo, también lo he visto bloquearse. Ojalá pudiera recordar las circunstancias del accidente. @abelenky es un lugar donde lo he visto colapsar.

Pudo haber estado donde se encuentra la subclase Fire, pero no se puede crear un destructor virtual (consulte a continuación). Cuando no tiene un destructor virtual, la función Update() llamará al ~Fire() en lugar del destructor apropiado ~Flame().

class Fire : Object 
{ 
public: 
    virtual void Update() 
    { 
     if(age > burnTime) 
     { 
      world.Remove(this); 
      delete this; //Make sure this is a virtual destructor. 
     } 
    } 
}; 

class Flame: public Fire 
{ 
private: 
    int somevariable; 
}; 
+0

Probablemente después de la llamada de eliminación, intentó acceder a una variable miembro o llamó a una función virtual –

+0

excelente ilustración de la necesidad de destructores virtuales. Up-modding usted. – abelenky

6

El borrado se producirá un error y puede hacer que el programa si el objeto era no asignado y construido con el nuevo. Esta construcción le impedirá instanciar la clase en la pila o estáticamente. En su caso, parece estar utilizando algún tipo de esquema de asignación. Si te apegas a eso, esto podría ser seguro. Sin embargo, ciertamente no lo haría.

+0

también - podría terminar con punteros colgantes de loadza – JohnIdol

30

Ha realizado Update una función virtual, lo que sugiere que las clases derivadas pueden anular la implementación de Update.Esto introduce dos grandes riesgos.

1.) Una implementación anulada puede recordar hacer un World.Remove, pero puede olvidarse el delete this. La memoria se filtró.

2.) La implementación reemplazada llama a la clase base Update, que hace un delete this, pero luego continúa con más trabajo, pero con un puntero this-no válido.

Considere este ejemplo:

class WizardsFire: public Fire 
{ 
public: 
    virtual void Update() 
    { 
     Fire::Update(); // May delete the this-object! 
     RegenerateMana(this.ManaCost); // this is now invaild! GPF! 
    } 
} 
+0

¡He visto esto suceder! No es divertido intentar localizarlo. –

+0

Ack, eso no es divertido. Gracias por señalar esto, 2 problemas que no tenemos (directamente) en C#. –

5

Ciertamente funciona para objetos creados por new y cuando la persona que llama de Update buena información acerca de ese comportamiento. Pero lo evitaría. En su caso, la propiedad claramente está en el mundo, por lo que haría que el mundo lo elimine. El objeto no se crea a sí mismo, creo que tampoco debería eliminarse. Puede ser muy sorprendente si llama a una función "Actualizar" en su objeto, pero de repente ese objeto ya no existe sin que World haga nada por sí mismo (aparte de Quitarlo de su lista, ¡pero en otro marco! La actualización del objeto no se dará cuenta de eso).

Algunas ideas sobre este

  1. Añadir una lista de referencias a objetos a la Humanidad. Cada objeto en esa lista está pendiente de eliminación. Esta es una técnica común, y se usa en wxWidgets para ventanas de nivel superior que se cerraron, pero aún pueden recibir mensajes. En tiempo de inactividad, cuando se procesan todos los mensajes, se procesa la lista pendiente y se eliminan los objetos. Creo que Qt sigue una técnica similar.
  2. Comenta al mundo que el objeto quiere ser eliminado. El mundo estará debidamente informado y se encargará de todo lo que se necesite hacer. Algo así como un deleteObject(Object&) tal vez.
  3. Agregue una función shouldBeDeleted a cada objeto que devuelve verdadero si el objeto desea ser eliminado por su propietario.

Preferiría la opción 3. El mundo llamaría Actualizar. Y después de eso, busca si el objeto debe eliminarse y puede hacerlo, o si lo desea, recuerda ese hecho al agregar ese objeto a una lista de eliminación pendiente de forma manual.

Es un dolor en el culo cuando no puede estar seguro de cuándo y cuándo no para poder acceder a las funciones y datos del objeto. Por ejemplo, en wxWidgets, hay una clase wxThread que puede operar en dos modos. Uno de estos modos (llamado "desmontable") es que si su función principal regresa (y los recursos de la secuencia deben liberarse), se elimina (para liberar la memoria ocupada por el objeto wxThread) en lugar de esperar al propietario del hilo objeto para llamar a una función de espera o unirse. Sin embargo, esto causa dolor de cabeza severo. Nunca puede llamar a ninguna función porque podría haberse cancelado en cualquier circunstancia, y no puede crearse con nueva. Mucha gente me dijo que no les gusta mucho su comportamiento.

La autodeclaración del objeto contado de referencia está oliendo, imho.Comparemos:

// bitmap owns the data. Bitmap keeps a pointer to BitmapData, which 
// is shared by multiple Bitmap instances. 
class Bitmap { 
    ~Bitmap() { 
     if(bmp->dec_refcount() == 0) { 
      // count drops to zero => remove 
      // ref-counted object. 
      delete bmp; 
     } 
    } 
    BitmapData *bmp; 
}; 

class BitmapData { 
    int dec_refcount(); 
    int inc_refcount(); 

}; 

compararlo con auto-eliminación de objetos refcounted:

class Bitmap { 
    ~Bitmap() { 
     bmp->dec_refcount(); 
    } 
    BitmapData *bmp; 
}; 

class BitmapData { 
    int dec_refcount() { 
     int newCount = --count; 
     if(newCount == 0) { 
      delete this; 
     } 
     return newCount; 
    } 
    int inc_refcount(); 
}; 

Creo que la primera es mucho más agradable, y creo que los objetos de referencia contado bien diseñados hacen no no "borrar esto ", porque aumenta el acoplamiento: la clase que utiliza los datos contados de referencia tiene que saber y recordar que los datos se eliminan a sí mismos como un efecto secundario de la disminución del recuento de referencias. Observe cómo "bmp" se convierte posiblemente en un puntero colgante en ~ destructor de Bitmap. Podría decirse que no hacer eso "eliminar esto" es mucho mejor aquí.

respuesta a una pregunta similar "What is the use of delete this"

+0

Me gusta esta respuesta y terminé usando elementos de ella, pero desafortunadamente solo puedo aceptar una. ¡Gracias! –

+0

me alegro de que te hayamos ayudado :) jalf tiene algunas ideas realmente buenas en su respuesta, creo. diviértete :) –

0

borrar esto es muy arriesgado. No hay nada "incorrecto" con eso. Es legal Pero hay tantas cosas que pueden salir mal cuando lo usa que deberían usarse con moderación.

Considere el caso en el que alguien tiene punteros abiertos o referencias al objeto, y luego se va y se elimina.

En la mayoría de los casos, creo que la vida del objeto debe ser administrada por el mismo objeto que la crea. Simplemente hace que sea más fácil saber dónde ocurre la destrucción.

2

Otros han mencionado problemas con "eliminar esto."En resumen, dado que" Mundo "maneja la creación de Fuego, también debería gestionar su eliminación.

Otro problema es si el mundo termina con un incendio aún encendido. Si este es el único mecanismo mediante el cual un fuego puede ser destruido, entonces podrías terminar con un huérfano o un incendio de basura.

Para la semántica que deseas, tendría una bandera "activa" o "viva" en tu clase "Fuego" (u Objeto si corresponde). entonces el mundo sería en ocasiones comprobar su inventario de objetos, y deshacerse de los que ya no están activos

-.

Una nota más es que su código, como está escrito, tiene Fire heredando de forma privada de Object, ya que es el valor predeterminado, aunque es mucho menos común que el público en espíritu. Probablemente deberías hacer explícito el tipo de herencia, y sospecho que realmente quieres la herencia pública.

+0

Nosotros no empezamos el fuego Fue siempre ardiendo Desde que el mundo ha estado convirtiendo No comenzamos el fuego No, no lo encendió Pero hemos tratado de luchar contra ella – abelenky

+0

Esta falta es pública porque mi cerebro todavía está en modo C# ... no había considerado cómo eliminar el fuego si el mundo termina primero ... ¡eso suena bastante serio! –

Cuestiones relacionadas