Pregunta

He estado usando mucho los punteros inteligentes (boost :: shared_ptr para ser exactos) en mis proyectos durante los últimos dos años. Entiendo y aprecio sus beneficios y generalmente me gustan mucho. Pero cuanto más los uso, más echo de menos el comportamiento determinista de C ++ con respecto a la gestión de memoria y RAII que parece que me gustan en un lenguaje de programación. Los punteros inteligentes simplifican el proceso de gestión de la memoria y proporcionan la recolección automática de basura, entre otras cosas, pero el problema es que el uso de la recolección automática de basura en general y el puntero inteligente introduce un cierto grado de indeterminación en el orden de (des) inicializaciones. Este indeterminismo quita el control a los programadores y, como me he dado cuenta últimamente, hace que el trabajo de diseño y desarrollo de API, cuyo uso no se conoce completamente de antemano en el momento del desarrollo, requiera mucho tiempo porque Todos los patrones de uso y los casos de esquina deben estar bien pensados.

Para elaborar más, actualmente estoy desarrollando una API. Partes de esta API requieren que ciertos objetos se inicialicen antes o se destruyan después de otros objetos. Dicho de otra manera, el orden de (des) inicialización es importante a veces. Para darle un ejemplo simple, digamos que tenemos una clase llamada Sistema. Un sistema proporciona algunas funciones básicas (registro en nuestro ejemplo) y contiene varios subsistemas a través de punteros inteligentes.

class System {
public:
    boost::shared_ptr< Subsystem > GetSubsystem( unsigned int index ) {
        assert( index < mSubsystems.size() );
        return mSubsystems[ index ];
    }

    void LogMessage( const std::string& message ) {
        std::cout << message << std::endl;
    }

private:
    typedef std::vector< boost::shared_ptr< Subsystem > > SubsystemList;
    SubsystemList mSubsystems;    
};

class Subsystem {
public:
    Subsystem( System* pParentSystem )
         : mpParentSystem( pParentSystem ) {
    }

    ~Subsystem() {
         pParentSubsystem->LogMessage( "Destroying..." );
         // Destroy this subsystem: deallocate memory, release resource, etc.             
    }

    /*
     Other stuff here
    */

private:
    System * pParentSystem; // raw pointer to avoid cycles - can also use weak_ptrs
};

Como ya se puede decir, un subsistema solo es significativo en el contexto de un sistema. Pero un subsistema en un diseño de este tipo puede sobrevivir fácilmente a su sistema principal.

int main() {
    {
        boost::shared_ptr< Subsystem > pSomeSubsystem;
        {
            boost::shared_ptr< System > pSystem( new System );
            pSomeSubsystem = pSystem->GetSubsystem( /* some index */ );

        } // Our System would go out of scope and be destroyed here, but the Subsystem that pSomeSubsystem points to will not be destroyed.

     } // pSomeSubsystem would go out of scope here but wait a second, how are we going to log messages in Subsystem's destructor?! Its parent System is destroyed after all. BOOM!

    return 0;
}

Si hubiésemos utilizado los punteros sin procesar para mantener los subsistemas, habríamos destruido los subsistemas cuando nuestro sistema se hubiera desactivado, por supuesto, pSomeSubsystem sería un puntero colgante.

Aunque no es tarea de un diseñador de API proteger a los programadores clientes de sí mismos, es una buena idea hacer que la API sea fácil de usar correctamente y difícil de usar de manera incorrecta. Así que les pregunto a ustedes, chicos. ¿Qué piensas? ¿Cómo debo aliviar este problema? ¿Cómo diseñarías un sistema así?

Gracias de antemano, Josh

¿Fue útil?

Solución

Resumen de problemas

Hay dos preocupaciones en conflicto en esta pregunta.

  1. Gestión del ciclo de vida de Subsystem s, permitiendo su eliminación en el momento adecuado.
  2. Los clientes de Subsystem s deben saber que el Subsystem que están utilizando es válido.

Manejo # 1

System posee los Subsystem y debe administrar su ciclo de vida con su propio alcance. El uso de shared_ptr para esto es particularmente útil, ya que simplifica la destrucción, pero no debe distribuirlos porque perderá el determinismo que está buscando con respecto a su dislocación.

Manejo # 2

Esta es la preocupación más interesante de abordar. Al describir el problema con más detalle, necesita que los clientes reciban un objeto que se comporte como un Subsystem mientras que ese Subsystem (y su System ) principal exista , pero se comporta adecuadamente después de que se destruya un Subsystem .

Esto se resuelve fácilmente mediante una combinación de Proxy Pattern , el State Pattern y el Patrón de objeto nulo . Si bien esto puede parecer una solución un poco compleja, ' Hay una simplicidad que solo se tiene en el otro lado de la complejidad .' Como desarrolladores de bibliotecas / API, debemos hacer un esfuerzo adicional para que nuestros sistemas sean robustos. Además, queremos que nuestros sistemas se comporten de manera intuitiva como lo espera un usuario, y que se deterioren con gracia cuando intentan usarlos mal. Hay muchas soluciones para este problema, sin embargo, esta debería llevarlo a ese punto tan importante donde, como usted y Scott Meyers dice, es " fácil de usar correctamente y difícil de usar incorrectamente. '

Ahora, estoy asumiendo que en realidad, System se ocupa de alguna clase base de Subsystem s, de la que derivan varios Subsystem s. Lo he presentado a continuación como SubsystemBase . Debe introducir un objeto Proxy , SubsystemProxy a continuación, que implementa la interfaz de SubsystemBase enviando las solicitudes al objeto es proxying. (En este sentido, es muy parecido a una aplicación de propósito especial de la Patrón decorador .) Cada Subsystem crea uno de estos objetos, que posee a través de un shared_ptr , y regresa cuando se solicita a través de GetProxy () , que es llamado por el objeto System principal cuando se llama a GetSubsystem () .

Cuando un Sistema sale de su alcance, cada uno de sus objetos Subsystem se destruye. En su destructor, llaman a mProxy- > Nullify () , lo que hace que sus objetos Proxy cambien su estado . Lo hacen cambiando para apuntar a un objeto nulo , que implementa la interfaz SubsystemBase , pero no hace nada.

El uso del patrón de estado aquí ha permitido que la aplicación cliente sea completamente ajena a la existencia o no de un Subsistema en particular. Además, no es necesario verificar los punteros o mantenerse cerca de las instancias que deberían haberse destruido.

El patrón de proxy permite al cliente depender de un objeto de peso ligero que envuelva por completo los detalles del funcionamiento interno de la API y mantenga una interfaz constante y uniforme .

El Patrón de objeto nulo permite que Proxy funcione después del subsistema original ha sido eliminado.

Código de ejemplo

Aquí había colocado un ejemplo de calidad de pseudo-código, pero no estaba satisfecho con él. Lo reescribí para que sea un ejemplo preciso y compilador (utilicé g ++) de lo que he descrito anteriormente. Para que funcionara, tuve que introducir algunas otras clases, pero sus usos deberían ser claros en sus nombres. Utilicé el Singleton Pattern para el NullSubsystem class, ya que tiene sentido que no necesitarías más de uno. ProxyableSubsystemBase abstrae completamente el comportamiento de Proxying del Subsystem , lo que le permite ignorar este comportamiento. Aquí está el diagrama UML de las clases:

 Diagrama UML de subsistema y jerarquía del sistema

Código de ejemplo:

#include <iostream>
#include <string>
#include <vector>

#include <boost/shared_ptr.hpp>


// Forward Declarations to allow friending
class System;
class ProxyableSubsystemBase;

// Base defining the interface for Subsystems
class SubsystemBase
{
  public:
    // pure virtual functions
    virtual void DoSomething(void) = 0;
    virtual int GetSize(void) = 0;

    virtual ~SubsystemBase() {} // virtual destructor for base class
};


// Null Object Pattern: an object which implements the interface to do nothing.
class NullSubsystem : public SubsystemBase
{
  public:
    // implements pure virtual functions from SubsystemBase to do nothing.
    void DoSomething(void) { }
    int GetSize(void) { return -1; }

    // Singleton Pattern: We only ever need one NullSubsystem, so we'll enforce that
    static NullSubsystem *instance()
    {
      static NullSubsystem singletonInstance;
      return &singletonInstance;
    }

  private:
    NullSubsystem() {}  // private constructor to inforce Singleton Pattern
};


// Proxy Pattern: An object that takes the place of another to provide better
//   control over the uses of that object
class SubsystemProxy : public SubsystemBase
{
  friend class ProxyableSubsystemBase;

  public:
    SubsystemProxy(SubsystemBase *ProxiedSubsystem)
      : mProxied(ProxiedSubsystem)
      {
      }

    // implements pure virtual functions from SubsystemBase to forward to mProxied
    void DoSomething(void) { mProxied->DoSomething(); }
    int  GetSize(void) { return mProxied->GetSize(); }

  protected:
    // State Pattern: the initial state of the SubsystemProxy is to point to a
    //  valid SubsytemBase, which is passed into the constructor.  Calling Nullify()
    //  causes a change in the internal state to point to a NullSubsystem, which allows
    //  the proxy to still perform correctly, despite the Subsystem going out of scope.
    void Nullify()
    {
        mProxied=NullSubsystem::instance();
    }

  private:
      SubsystemBase *mProxied;
};


// A Base for real Subsystems to add the Proxying behavior
class ProxyableSubsystemBase : public SubsystemBase
{
  friend class System;  // Allow system to call our GetProxy() method.

  public:
    ProxyableSubsystemBase()
      : mProxy(new SubsystemProxy(this)) // create our proxy object
    {
    }
    ~ProxyableSubsystemBase()
    {
      mProxy->Nullify(); // inform our proxy object we are going away
    }

  protected:
    boost::shared_ptr<SubsystemProxy> GetProxy() { return mProxy; }

  private:
    boost::shared_ptr<SubsystemProxy> mProxy;
};


// the managing system
class System
{
  public:
    typedef boost::shared_ptr< SubsystemProxy > SubsystemHandle;
    typedef boost::shared_ptr< ProxyableSubsystemBase > SubsystemPtr;

    SubsystemHandle GetSubsystem( unsigned int index )
    {
        assert( index < mSubsystems.size() );
        return mSubsystems[ index ]->GetProxy();
    }

    void LogMessage( const std::string& message )
    {
        std::cout << "  <System>: " << message << std::endl;
    }

    int AddSubsystem( ProxyableSubsystemBase *pSubsystem )
    {
      LogMessage("Adding Subsystem:");
      mSubsystems.push_back(SubsystemPtr(pSubsystem));
      return mSubsystems.size()-1;
    }

    System()
    {
      LogMessage("System is constructing.");
    }

    ~System()
    {
      LogMessage("System is going out of scope.");
    }

  private:
    // have to hold base pointers
    typedef std::vector< boost::shared_ptr<ProxyableSubsystemBase> > SubsystemList;
    SubsystemList mSubsystems;
};

// the actual Subsystem
class Subsystem : public ProxyableSubsystemBase
{
  public:
    Subsystem( System* pParentSystem, const std::string ID )
      : mParentSystem( pParentSystem )
      , mID(ID)
    {
         mParentSystem->LogMessage( "Creating... "+mID );
    }

    ~Subsystem()
    {
         mParentSystem->LogMessage( "Destroying... "+mID );
    }

    // implements pure virtual functions from SubsystemBase
    void DoSomething(void) { mParentSystem->LogMessage( mID + " is DoingSomething (tm)."); }
    int GetSize(void) { return sizeof(Subsystem); }

  private:
    System * mParentSystem; // raw pointer to avoid cycles - can also use weak_ptrs
    std::string mID;
};



//////////////////////////////////////////////////////////////////
// Actual Use Example
int main(int argc, char* argv[])
{

  std::cout << "main(): Creating Handles H1 and H2 for Subsystems. " << std::endl;
  System::SubsystemHandle H1;
  System::SubsystemHandle H2;

  std::cout << "-------------------------------------------" << std::endl;
  {
    std::cout << "  main(): Begin scope for System." << std::endl;
    System mySystem;
    int FrankIndex = mySystem.AddSubsystem(new Subsystem(&mySystem, "Frank"));
    int ErnestIndex = mySystem.AddSubsystem(new Subsystem(&mySystem, "Ernest"));

    std::cout << "  main(): Assigning Subsystems to H1 and H2." << std::endl;
    H1=mySystem.GetSubsystem(FrankIndex);
    H2=mySystem.GetSubsystem(ErnestIndex);


    std::cout << "  main(): Doing something on H1 and H2." << std::endl;
    H1->DoSomething();
    H2->DoSomething();
    std::cout << "  main(): Leaving scope for System." << std::endl;
  }
  std::cout << "-------------------------------------------" << std::endl;
  std::cout << "main(): Doing something on H1 and H2. (outside System Scope.) " << std::endl;
  H1->DoSomething();
  H2->DoSomething();
  std::cout << "main(): No errors from using handles to out of scope Subsystems because of Proxy to Null Object." << std::endl;

  return 0;
}

Salida del código:

main(): Creating Handles H1 and H2 for Subsystems.
-------------------------------------------
  main(): Begin scope for System.
  <System>: System is constructing.
  <System>: Creating... Frank
  <System>: Adding Subsystem:
  <System>: Creating... Ernest
  <System>: Adding Subsystem:
  main(): Assigning Subsystems to H1 and H2.
  main(): Doing something on H1 and H2.
  <System>: Frank is DoingSomething (tm).
  <System>: Ernest is DoingSomething (tm).
  main(): Leaving scope for System.
  <System>: System is going out of scope.
  <System>: Destroying... Frank
  <System>: Destroying... Ernest
-------------------------------------------
main(): Doing something on H1 and H2. (outside System Scope.)
main(): No errors from using handles to out of scope Subsystems because of Proxy to Null Object.

Otros pensamientos:

  • Un artículo interesante que leí en uno de los libros de Game Programming Gems habla sobre el uso de objetos nulos para la depuración y el desarrollo. Hablaban específicamente sobre el uso de texturas y modelos de gráficos nulos, como una textura de tablero de ajedrez para hacer que los modelos faltantes realmente se destaquen. Lo mismo podría aplicarse aquí cambiando el NullSubsystem para un ReportingSubsystem que registraría la llamada y posiblemente la pila de llamadas cada vez que se acceda a ella. Esto le permitiría a usted o a los clientes de su biblioteca rastrear dónde están dependiendo de algo que haya quedado fuera del alcance, pero sin la necesidad de causar un bloqueo.

  • En un comentario @Arkadiy mencioné que la dependencia circular que surgió entre System y Subsystem es un poco desagradable. Se puede remediar fácilmente haciendo que System se derive de una interfaz de la que depende Subsystem , una aplicación de Robert C Martin's Principio de inversión de dependencia . Mejor aún sería aislar la funcionalidad que Subsystem necesita de sus padres, escribir una interfaz para eso, luego aferrarse a un implementador de esa interfaz en System y pasarla a los Subsystem s, que lo mantendrían a través de un shared_ptr . Por ejemplo, podría tener LoggerInterface , que su Subsystem usa para escribir en el registro, luego podría derivar CoutLogger o FileLogger de él, y mantenga una instancia de este en System . Eliminar la dependencia circular

Otros consejos

Esto se puede hacer con el uso adecuado de la clase weak_ptr . De hecho, ya estás bastante cerca de tener una buena solución. Tiene razón en que no se puede esperar que " supere-piense " no debe esperar que siempre sigan las " reglas " de su API (como estoy seguro de que ya están conscientes). Entonces, lo mejor que puedes hacer es controlar los daños.

Recomiendo que su llamada a GetSubsystem devuelva un weak_ptr en lugar de un shared_ptr simplemente para que el desarrollador del cliente pueda probar la validez del puntero sin reclamar siempre una referencia a él.

Del mismo modo, haga que pParentSystem sea boost :: weak_ptr < System > para que pueda detectar internamente si su Sistema principal todavía existe a través de una llamada a lock en pParentSystem junto con una verificación de NULL (un puntero en bruto no le dirá esto).

Suponiendo que cambie su clase Subsystem para verificar siempre si existe o no su objeto System correspondiente, puede asegurarse de que si el programador cliente intenta usar el Objeto del subsistema fuera del alcance previsto que provocará un error (que usted controla), en lugar de una excepción inexplicable (que debe confiar en que el programador cliente detecte / maneje adecuadamente).

Por lo tanto, en su ejemplo con main () , las cosas no van a ser BOOM! La forma más elegante de manejar esto en el dtor de Subsystem sería hacer que se vea algo como esto:

class Subsystem
{
...
  ~Subsystem() {
       boost::shared_ptr<System> my_system(pParentSystem.lock());

       if (NULL != my_system.get()) {  // only works if pParentSystem refers to a valid System object
         // now you are guaranteed this will work, since a reference is held to the System object
         my_system->LogMessage( "Destroying..." );
       }
       // Destroy this subsystem: deallocate memory, release resource, etc.             

       // when my_system goes out of scope, this may cause the associated System object to be destroyed as well (if it holds the last reference)
  }
...
};

Espero que esto ayude!

Aquí, el sistema claramente posee los subsistemas y no veo ningún sentido en tener propiedad compartida. Simplemente devolvería un puntero crudo. Si un subsistema sobrevive a su sistema, es un error en sí mismo.

Tenías razón al principio de tu primer párrafo. Sus diseños basados ??en RAII (como el mío y el código C ++ más bien escrito) requieren que sus objetos estén en manos de punteros de propiedad exclusiva. En Boost eso sería scoped_ptr.

Entonces, ¿por qué no usaste scoped_ptr. Seguramente será porque quería los beneficios de weak_ptr para protegerse contra las referencias pendientes, pero solo puede apuntar un weak_ptr a shared_ptr. Así que ha adoptado la práctica común de declarar de forma expedita shared_ptr cuando lo que realmente deseaba era la propiedad individual. Esta es una declaración falsa y, como usted dice, compromete que se llame a los destructores en la secuencia correcta. Por supuesto, si nunca comparte la propiedad, se saldrá con la suya, pero tendrá que verificar constantemente todo su código para asegurarse de que nunca se compartió.

Para empeorar las cosas, el boost :: weak_ptr es inconveniente de usar (no tiene operador - >), por lo que los programadores evitan este inconveniente al declarar falsamente las referencias de observación pasiva como shared_ptr. Por supuesto, esto comparte la propiedad y si olvida anular ese shared_ptr, entonces su objeto no se destruirá o se llamará a su destructor cuando lo intente.

En resumen, la biblioteca de impulso le ha impedido hacerlo: no logra adoptar buenas prácticas de programación en C ++ y obliga a los programadores a hacer declaraciones falsas para intentar obtener algún beneficio de ello. Solo es útil para el código de pegado de scripts que realmente quiere una propiedad compartida y no está interesado en un control estricto de la memoria o que se llame a los destructores en la secuencia correcta.

He estado en el mismo camino que tú. La protección contra los punteros colgantes es muy necesaria en C ++, pero la biblioteca boost no proporciona una solución aceptable. Tuve que resolver este problema: mi departamento de software quería garantías de que C ++ se puede hacer seguro. Así que hice mi propia cuenta, era bastante trabajo y se puede encontrar en:

http://www.codeproject.com/KB/cpp/XONOR.aspx

Es totalmente adecuado para el trabajo de subprocesos individuales y estoy a punto de actualizarlo para incluir los punteros que se comparten entre los subprocesos. Su característica clave es que admite observadores pasivos inteligentes (autoceros) de objetos de propiedad exclusiva.

Desafortunadamente, los programadores se han dejado seducir por la recolección de basura y las soluciones de punteros inteligentes y, en gran medida, ni siquiera están pensando en la propiedad y los observadores pasivos, como resultado, ni siquiera saben que lo que están haciendo es mal y no te quejes ¡La herejía contra Boost es casi desconocida!

Las soluciones que se te han sugerido son absurdamente complicadas y de ninguna ayuda. Son ejemplos del absurdo que resulta de una renuencia cultural a reconocer que los punteros a objetos tienen roles distintos que deben declararse correctamente y una fe ciega de que Boost debe ser la solución.

No veo un problema con el hecho de que System :: GetSubsystem devuelva un puntero sin procesar (más peligroso que un puntero inteligente) a un subsistema. Como el cliente no es responsable de construir los objetos, entonces no hay un contrato implícito para que el cliente sea responsable de la limpieza. Y como es una referencia interna, debería ser razonable suponer que la vida útil del objeto Subsistema depende de la vida útil del objeto Sistema. Luego debe reforzar este contrato implícito con la documentación que lo indique.

El punto es que no estás reasignando ni compartiendo la propiedad, ¿por qué usar un puntero inteligente?

El problema real aquí es su diseño. No hay una buena solución, porque el modelo no refleja buenos principios de diseño. Aquí hay una regla práctica que uso:

  • Si un objeto tiene una colección de otros objetos y puede devolver cualquier objeto arbitrario de esa colección, entonces elimine ese objeto de su diseño .

Me doy cuenta de que tu ejemplo está diseñado, pero es un antipatrón que veo mucho en el trabajo. Pregúntese, qué valor tiene System agregando que std :: vector < shared_ptr < SubSystem > > ¿no? Los usuarios de su API necesitan conocer la interfaz de SubSystem (ya que los devuelve), por lo que escribir un titular para ellos solo agrega complejidad. Al menos la gente conoce la interfaz de std :: vector , lo que los obliga a recordar el GetSubsystem () sobre en () o operador [ ] es solo significado .

Su pregunta es acerca de la administración de la vida útil de los objetos, pero una vez que comienza a distribuir objetos, pierde el control de la vida útil al permitir que otros los mantengan vivos ( shared_ptr ) o corre el riesgo de que se bloqueen si se usan. después de que se han ido (punteros en bruto). En aplicaciones de subprocesos múltiples es aún peor: ¿quién bloquea los objetos que está entregando a diferentes subprocesos? Los impulsos compartidos y los punteros débiles son una trampa que induce a la complejidad cuando se usan de esta manera, especialmente porque son solo lo suficientemente seguros como para hacer tropezar a los desarrolladores sin experiencia.

Si va a crear un titular, debe ocultar la complejidad de sus usuarios y liberarlos de las cargas que puede manejar usted mismo. Como ejemplo, una interfaz que consiste en a) enviar un comando al subsistema (por ejemplo, un URI - / system / subsystem / command? Param = value) yb) subsistemas iterativos y comandos del subsistema (a través de un iterador similar a stl) y posiblemente c) el subsistema de registro le permitiría ocultar casi todos los detalles de su implementación a sus usuarios y hacer cumplir los requisitos de vida útil / pedido / bloqueo internamente.

Una API iterable / enumerable es muy preferible a exponer objetos en cualquier caso: los comandos / registros podrían ser fácilmente serializados para generar casos de prueba o archivos de configuración, y se pueden mostrar de forma interactiva (por ejemplo, en un control de árbol, con diálogos). compuesto por la consulta de las acciones / parámetros disponibles). También estaría protegiendo a los usuarios de su API de los cambios internos que deba realizar en las clases del subsistema.

Te advierto que no sigas los consejos de la respuesta de Aaron. Diseñar una solución a un problema tan simple que requiere 5 patrones de diseño diferentes para implementar solo puede significar que se está resolviendo el problema incorrecto. También estoy cansado de cualquiera que cita al Sr. Myers en relación con el diseño, ya que él mismo lo admite:

"No he escrito software de producción en más de 20 años, y nunca he escrito software de producción en C ++. No, no nunca. Además, nunca he intentado escribir software de producción en C ++, así que no solo no soy un verdadero desarrollador de C ++, ni siquiera soy un aspirante. Contrarrestando esto ligeramente es el hecho de que escribí un software de investigación en C ++ durante mis años de posgrado (1985-1993), pero incluso eso fue algo pequeño (unos pocos miles de líneas) para desarrolladores que se desechan rápidamente. Y desde que me convertí en consultor hace más de una docena de años, mi programación en C ++ se ha limitado a jugar a "veamos cómo funciona esto" (o, a veces, "veamos cuántos compiladores se rompe"), programas que suelen encajar. un solo archivo " ;.

No quiere decir que no vale la pena leer sus libros, pero no tiene autoridad para hablar sobre diseño o complejidad.

En su ejemplo, sería mejor si el sistema tuviera un vector < Subsystem > en lugar de un vector < shared_ptr < Subsystem > > . Es a la vez más simple y elimina la preocupación que tiene. GetSubsystem devolvería una referencia en su lugar.

Los objetos de la pila se lanzarán en el orden opuesto al que se crearon las instancias, por lo que, a menos que el desarrollador que usa la API esté tratando de administrar el puntero inteligente, normalmente no será un problema. Hay algunas cosas que no podrá prevenir, lo mejor que puede hacer es proporcionar advertencias en tiempo de ejecución, preferiblemente solo depuración.

Su ejemplo se parece mucho a COM, tiene un recuento de referencias de los subsistemas que se devuelven mediante shared_ptr, pero falta en el objeto del sistema en sí.

Si cada uno de los objetos del subsistema hizo una adición al objeto del sistema en la creación, y una versión en destrucción, al menos podría mostrar una excepción si el recuento de referencia era incorrecto cuando el objeto del sistema se destruye antes.

El uso de weak_ptr también le permitiría proporcionar un mensaje en su lugar / así como hacer estallar cuando las cosas se liberan en el orden incorrecto.

La esencia de su problema es una referencia circular: Sistema se refiere a Subsistema, y ??Subsistema, a su vez, se refiere a Sistema. Este tipo de estructura de datos no se puede manejar fácilmente mediante el recuento de referencias, ya que requiere una recolección de basura adecuada. Está intentando romper el bucle utilizando un puntero en bruto para uno de los bordes, esto solo producirá más complicaciones.

Se han sugerido al menos dos buenas soluciones, por lo que no intentaré superar los pósters anteriores. Solo puedo señalar que en la solución de @ Aaron puede tener un proxy para el sistema en lugar de subsistemas, dependiendo de lo que sea más complejo y lo que tenga sentido.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top