Вопрос об умных указателях и их неизбежном индетерминизме - PullRequest
16 голосов
/ 30 декабря 2008

Последние два года я широко использовал умные указатели (точнее, boost :: shared_ptr) в своих проектах. Я понимаю и ценю их преимущества, и в целом они мне очень нравятся. Но чем больше я их использую, тем больше я скучаю по детерминированному поведению C ++ относительно управления памятью и RAII, которое мне, похоже, нравится в языке программирования. Интеллектуальные указатели упрощают процесс управления памятью и обеспечивают автоматический сбор мусора, но проблема заключается в том, что использование автоматического сбора мусора в целом и интеллектуального указателя вносит определенную степень неопределенности в порядке (де) инициализации. Этот индетерминизм отнимает контроль у программистов и, как я понял в последнее время, делает работу по проектированию и разработке API, использование которой заранее неизвестно во время разработки, отнимает много времени, так как все схемы использования и угловые случаи должны быть хорошо продуманы.

Чтобы уточнить, я сейчас занимаюсь разработкой API. Части этого API требуют, чтобы определенные объекты были инициализированы до или уничтожены после других объектов. Иными словами, порядок (де) инициализации важен временами. Чтобы дать вам простой пример, допустим, у нас есть класс System. Система предоставляет некоторые базовые функциональные возможности (регистрация в нашем примере) и содержит ряд подсистем с помощью интеллектуальных указателей.

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
};

Как вы уже можете сказать, подсистема имеет смысл только в контексте системы. Но Подсистема в таком дизайне может легко пережить свою родительскую Систему.

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;
}

Если бы мы использовали необработанные указатели для хранения подсистем, мы бы уничтожили подсистемы, когда наша система вышла из строя, тогда pSomeSubsystem была бы висящим указателем.

Хотя разработчик API не должен защищать клиентских программистов от самих себя, это хорошая идея - сделать API простым в использовании и трудным в использовании. Итак, я спрашиваю вас, ребята. Как вы думаете? Как мне решить эту проблему? Как бы вы спроектировали такую ​​систему?

Заранее спасибо, Джош

Ответы [ 9 ]

27 голосов
/ 31 декабря 2008

Краткое описание проблемы

В этом вопросе есть две конкурирующие проблемы.

  1. Управление жизненным циклом Subsystem с, что позволяет их удалить в нужное время.
  2. Клиенты Subsystem должны знать, что Subsystem, который они используют, действителен.

Обработка # 1

System владеет Subsystem s и должен управлять их жизненным циклом со своими собственными возможностями. Использование shared_ptr s для этого особенно полезно, поскольку это упрощает разрушение, но вы не должны раздавать их, потому что тогда вы теряете детерминизм, который вы ищете в отношении их освобождения.

Обработка # 2

Это более интересная проблема для решения. Для более подробного описания проблемы вам необходимо, чтобы клиенты получали объект, который ведет себя как Subsystem, в то время как этот Subsystem (и его родитель System) существует, но ведет себя соответствующим образом после уничтожения Subsystem.

Это легко решается с помощью комбинации Proxy Pattern , State State и Null Object Pattern . Хотя это может показаться немного сложным решением, « Существует простота, которую нужно иметь с другой стороны сложности ». Как разработчики библиотек / API, мы должны приложить дополнительные усилия, чтобы сделать наши системы надежными. Кроме того, мы хотим, чтобы наши системы вели себя интуитивно, как того ожидает пользователь, и изящно затухали, когда они пытались их неправильно использовать. Есть много решений этой проблемы, однако этот должен привести вас ко всему важному моменту, когда, как вы и Скотт Мейерс говорите, " легко использовать правильно и трудно использовать неправильно .

Теперь я предполагаю, что на самом деле System имеет дело с некоторым базовым классом Subsystem с, из которого вы получаете различные Subsystem с. Я представил это ниже как SubsystemBase. Вам необходимо представить объект Proxy , ниже SubsystemProxy, который реализует интерфейс SubsystemBase путем пересылки запросов к объекту, который он передает. (В этом смысле это очень похоже на специальное приложение Pattern Decorator .) Каждый Subsystem создает один из этих объектов, который он удерживает с помощью shared_ptr, и возвращает по запросу через GetProxy(), который вызывается родительским объектом System при вызове GetSubsystem().

Когда System выходит из области видимости, каждый из его Subsystem объектов разрушается. В своем деструкторе они вызывают mProxy->Nullify(), что заставляет их Proxy объектов менять их State . Они делают это, изменяя указатель на Null Object , который реализует интерфейс SubsystemBase, но делает это, ничего не делая.

Использование Pattern State здесь позволило клиентскому приложению полностью забыть о том, существует ли конкретный Subsystem. Более того, ему не нужно проверять указатели или хранить экземпляры, которые должны были быть уничтожены.

Proxy Pattern позволяет клиенту зависеть от легковесного объекта, который полностью оборачивает детали внутренней работы API и поддерживает постоянный, унифицированный интерфейс.

Шаблон Null Object позволяет Proxy функционировать после удаления исходного Subsystem.

Пример кода

Я разместил здесь примерный пример качества псевдокода, но меня это не удовлетворило. Я переписал это, чтобы быть точным, компилирующим (я использовал g ++) примером того, что я описал выше. Чтобы заставить его работать, мне пришлось представить несколько других классов, но их использование должно быть ясно из их названий. Я использовал Singleton Pattern для класса NullSubsystem, так как имеет смысл, что вам не понадобится больше одного. ProxyableSubsystemBase полностью абстрагирует поведение Proxying от Subsystem, позволяя ему не знать об этом. Вот диаграмма UML классов:

UML Diagram of Subsystem and System Hierarchy

Пример кода:

#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;
}

Вывод из кода:

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.

Другие мысли:

  • Интересная статья, которую я прочитал в одной из книг Game Programming Gems, рассказывает об использовании нулевых объектов для отладки и разработки. Они конкретно говорили об использовании нулевых графических моделей и текстур, таких как текстура шахматной доски, для того, чтобы недостающие модели действительно выделялись. То же самое можно применить здесь, заменив NullSubsystem для ReportingSubsystem, который бы регистрировал вызов и, возможно, стек вызовов при каждом обращении к нему. Это позволило бы вам или клиентам вашей библиотеки отследить, где они находятся, в зависимости от того, что вышло из области видимости, но без необходимости вызывать сбой.

  • Я упомянул в комментарии @Arkadiy, что круговая зависимость, которую он вывел между System и Subsystem, немного неприятна. Это можно легко исправить, если System получить от интерфейса, от которого зависит Subsystem, то есть от применения Принципа обращения зависимостей Роберта С. Мартина . Еще лучше было бы изолировать функциональность, которая нужна Subsystem s, от их родителя, написать интерфейс для этого, затем удержать разработчик этого интерфейса в System и передать его Subsystem s, который бы удерживал его через shared_ptr. Например, у вас может быть LoggerInterface, который ваш Subsystem использует для записи в журнал, затем вы можете извлечь CoutLogger или FileLogger из него и сохранить его экземпляр в System.
    Eliminating the Circular Dependency

11 голосов
/ 30 декабря 2008

Это возможно при правильном использовании класса weak_ptr. На самом деле, вы уже достаточно близки к тому, чтобы найти хорошее решение. Вы правы в том, что нельзя ожидать, что вы «переоцените» своих клиентских программистов, и при этом вы не должны ожидать, что они всегда будут следовать «правилам» вашего API (как я уверен, вы уже знаете). Итак, лучшее, что вы можете сделать на самом деле, это контроль урона.

Я рекомендую, чтобы ваш вызов GetSubsystem возвратил weak_ptr, а не shared_ptr просто, чтобы разработчик клиента мог проверить достоверность указателя, не всегда требуя ссылки на него.

Аналогичным образом, pParentSystem должен быть boost::weak_ptr<System>, чтобы он мог внутренне определить, существует ли его родительский элемент System, посредством вызова lock на pParentSystem вместе с проверкой NULL (необработанный указатель не скажет вам этого).

Предполагая, что вы измените свой класс Subsystem, чтобы всегда проверять, существует ли соответствующий ему объект System, вы можете быть уверены, что если клиентский программист попытается использовать объект Subsystem за пределами предполагаемой области, то ошибка будет результат (который вы контролируете), а не необъяснимое исключение (которое вы должны доверять клиентскому программисту, чтобы поймать / правильно обработать).

Итак, в вашем примере с main() все не будет БУМ! Самый изящный способ справиться с этим в dtor'е Subsystem состоит в том, чтобы он выглядел примерно так:

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)
  }
...
};

Надеюсь, это поможет!

4 голосов
/ 30 декабря 2008

Здесь System явно владеет подсистемами, и я не вижу смысла в совместном владении. Я бы просто вернул необработанный указатель. Если Подсистема переживает свою Систему, это ошибка сама по себе.

3 голосов
/ 05 августа 2009

Вы были в самом начале в первом абзаце. Ваши проекты, основанные на RAII (как мой и наиболее хорошо написанный код C ++), требуют, чтобы ваши объекты содержались указателями исключительной собственности. В Boost это будет scoped_ptr.

Так почему же вы не использовали scoped_ptr. Это, безусловно, будет потому, что вы хотели, чтобы преимущества слабого_птр защищали от висячих ссылок, но вы можете указать только слабый_птр на shared_ptr. Таким образом, вы применили обычную практику целесообразного объявления shared_ptr, когда вы действительно хотели единоличного владения. Это ложное объявление, и, как вы говорите, оно компрометирует деструкторы, вызываемые в правильной последовательности. Конечно, если вы никогда не поделитесь правами собственности, вам это сойдет с рук, но вам придется постоянно проверять весь ваш код, чтобы убедиться, что он никогда не передавался.

Что еще хуже, boost :: weak_ptr неудобно использовать (у него нет оператора ->), поэтому программисты избегают этого неудобства, ошибочно объявляя пассивные ссылки наблюдения как shared_ptr. Это, конечно, разделяет владение, и если вы забудете обнулить этот shared_ptr, ваш объект не будет уничтожен или его деструктор не будет вызван, когда вы намереваетесь.

Короче говоря, вы были обмануты библиотекой boost - она ​​не в состоянии принять хорошие методы программирования на C ++ и вынуждает программистов делать ложные декларации, чтобы попытаться извлечь из этого некоторую пользу. Это полезно только для написания скриптового кода, который действительно хочет совместного владения и не заинтересован в жестком контроле памяти или вызове деструкторов в правильной последовательности.

Я был на том же пути, что и вы. Защита от висячих указателей крайне необходима в C ++, но библиотека boost не обеспечивает приемлемого решения. Мне пришлось решить эту проблему - мой отдел программного обеспечения хотел получить гарантии того, что C ++ может быть безопасным. Таким образом, я свернул свой собственный - это было довольно много работы и может быть найдено в:

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

Этого вполне достаточно для однопоточной работы, и я собираюсь обновить его, чтобы охватить указатели, разделяемые между потоками. Его ключевая особенность заключается в том, что он поддерживает интеллектуальных (самообнуляющихся) пассивных наблюдателей исключительно принадлежащих объектов.

К сожалению, программисты были соблазнены сборкой мусора и «умными указателями» на все случаи и в значительной степени даже не думают о владении и пассивных наблюдателях - в результате они даже не знают, что они делают неправильно и не жалуйся. Ересь против Буста почти не слышна!

Решения, которые вам предложили, нелепо сложны и ничем не помогают. Они являются примерами абсурда, возникающего в результате культурного нежелания признать, что указатели объектов играют разные роли, которые должны быть правильно объявлены, и слепую веру в то, что повышение должно быть решением.

2 голосов
/ 30 мая 2009

Настоящая проблема здесь - ваш дизайн. Нет хорошего решения, потому что модель не отражает хорошие принципы дизайна. Вот удобное правило, которым я пользуюсь:

  • Если объект содержит коллекцию других объектов и может вернуть любой произвольный объект из этой коллекции, удалит этот объект из вашего проекта .

Я понимаю, что ваш пример надуман, но его анти-паттерн я часто вижу на работе. Спросите себя, какое значение System добавляет, что std::vector< shared_ptr<SubSystem> > нет? Пользователям вашего API необходимо знать интерфейс SubSystem (поскольку вы их возвращаете), поэтому написание держателя для них только добавляет сложности. По крайней мере, люди знают интерфейс к std::vector, заставляя их помнить GetSubsystem() выше at() или operator[] - это просто означает .

Ваш вопрос касается управления временем жизни объектов, но как только вы начинаете раздавать объекты, вы либо теряете контроль над временем жизни, позволяя другим сохранять их живыми (shared_ptr), либо рискуете потерпеть крах, если они будут использованы после того, как они уйдут (сырые указатели). В многопоточных приложениях еще хуже - кто блокирует объекты, которые вы раздаете разным потокам? Усиление общих и слабых указателей является ловушкой, вызывающей сложность, когда используется таким образом, тем более что они достаточно поточно безопасны, чтобы сбить с толку неопытных разработчиков.

Если вы собираетесь создать владельца, он должен скрывать сложности от ваших пользователей и избавлять их от бремени, которым вы можете управлять самостоятельно. В качестве примера, интерфейс, состоящий из: а) отправки команды в подсистему (например, URI - / system / subsystem / command? Param = value) и б) итерации подсистем и команд подсистемы (через stl-подобный итератор) и, возможно, в) Подсистема регистрации позволит вам скрыть почти все детали вашей реализации от ваших пользователей и обеспечить внутренние требования к сроку службы / упорядочению / блокировке.

Итеративный / перечислимый API-интерфейс значительно предпочтительнее, чем показ объектов в любом случае - команды / регистрации можно легко сериализовать для создания тестовых случаев или файлов конфигурации, и они могут отображаться в интерактивном режиме (скажем, в древовидном элементе управления, с диалоговыми окнами). составляется путем запроса доступных действий / параметров). Вы также будете защищать своих пользователей API от внутренних изменений, которые вам могут потребоваться внести в классы подсистем.

Я бы предостерег вас от следования совету в ответе Аарона. Разработка такого простого решения проблемы, для реализации которого требуется 5 различных шаблонов проектирования, может означать, что решается не та проблема. Я также устал от любого, кто цитирует мистера Майерса в связи с дизайном, так как по его собственному признанию:

"Я не писал производственное программное обеспечение более 20 лет, и я никогда не писал производственное программное обеспечение на C ++. Нет, никогда. Более того, я никогда даже не пытался писать производственное программное обеспечение на C ++, поэтому я не только Я не настоящий разработчик C ++, я даже не подражатель. Слегка уравновешивая это тот факт, что я писал исследовательские программы на C ++ в годы обучения в аспирантуре (1985-1993), но даже это было мало (несколько тысяч строк) Единственный разработчик, который можно быстро выбросить. И с тех пор, как более десятка лет назад он был консультантом, мое программирование на C ++ было ограничено игрушкой «посмотрим, как это работает» (или, иногда, «посмотрим сколько компиляторов это разбивает ») программы, обычно это программы, помещающиеся в один файл".

Не сказать, что его книги не стоит читать, но он не имеет права говорить о дизайне или сложности.

2 голосов
/ 30 декабря 2008

Я не вижу проблемы с тем, чтобы System :: GetSubsystem возвращала подсистему необработанный указатель (скорее, чем умный указатель). Поскольку клиент не несет ответственности за создание объектов, не существует неявного контракта для клиента, который будет нести ответственность за очистку. А поскольку это внутренняя ссылка, разумно предположить, что время жизни объекта Subsystem зависит от времени жизни объекта System. Затем вам следует закрепить этот подразумеваемый контракт документацией, в которой указывается столько же.

Дело в том, что вы не передаете или не передаете право собственности - так зачем использовать умный указатель?

1 голос
/ 31 декабря 2008

Стековые объекты будут выпущены в обратном порядке, в котором они были созданы, поэтому, если разработчик, использующий API, не пытается управлять умным указателем, обычно это не будет проблемой. Есть только некоторые вещи, которые вы не сможете предотвратить, лучшее, что вы можете сделать, - это предоставлять предупреждения во время выполнения, желательно только для отладки.

Ваш пример мне очень напоминает COM, у вас есть подсчет ссылок на подсистемы, возвращаемые с помощью shared_ptr, но вы упускаете его на самом системном объекте.

Если каждый из объектов подсистемы при создании создавал addref для системного объекта, а в выпуске для уничтожения вы могли бы по крайней мере отобразить исключение, если подсчет ссылок был неправильным, когда системный объект был уничтожен раньше.

Использование Слабого_птр также позволит вам вместо сообщения предоставить сообщение / взорвать, когда вещи освобождаются в неправильном порядке.

1 голос
/ 30 декабря 2008

В вашем примере было бы лучше, если бы система содержала vector<Subsystem>, а не vector<shared_ptr<Subsystem> >. Это и проще, и устраняет беспокойство у вас есть. Вместо этого GetSubsystem возвращает ссылку.

0 голосов
/ 31 декабря 2008

Суть вашей проблемы - циклическая ссылка: Система относится к Подсистеме, а Подсистема, в свою очередь, относится к Системе. Этот тип структуры данных не может быть легко обработан подсчетом ссылок - он требует правильной сборки мусора. Вы пытаетесь разорвать цикл, используя необработанный указатель для одного из ребер - это только усложнит задачу.

Было предложено как минимум два хороших решения, поэтому я не буду пытаться превзойти предыдущие постеры. Могу только отметить, что в решении @ Aaron вы можете иметь прокси для Системы, а не для Подсистем - в зависимости от того, что является более сложным и что имеет смысл.

...