当前位置:  开发笔记 > 编程语言 > 正文

关于智能指针的一个问题及其必然的非决定论

如何解决《关于智能指针的一个问题及其必然的非决定论》经验,为你挑选了2个好方法。

在过去的两年里,我一直在我的项目中广泛使用智能指针(boost :: shared_ptr).我理解并欣赏他们的好处,我一般都喜欢他们.但是我越是使用它们,我就越想念C++的确定性行为,关于内存管理和RAII,我似乎更喜欢编程语言.智能指针简化了内存管理的过程并提供了自动垃圾收集等功能,但问题是一般使用自动垃圾收集和智能指针特别引入了(de)初始化顺序的某种程度的不确定性.这种不确定性使控制权远离程序员,并且正如我最近意识到的那样,它完成了设计和开发API的工作,

为了详细说明,我目前正在开发一个API.此API的某些部分要求在其他对象之前初始化或销毁某些对象.换句话说,(de)初始化的顺序有时很重要.举个简单的例子,假设我们有一个名为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易于正确使用且难以正确使用.所以我问你们.你怎么看?我该如何缓解这个问题?你会如何设计这样的系统?

先谢谢你,乔希



1> Aaron..:

问题摘要

这个问题有两个相互竞争的问题.

    Subsystems的生命周期管理,允许在适当的时间将其删除.

    Subsystems的客户需要知道Subsystem他们使用的是有效的.

处理#1

System拥有Subsystems并且应该用它自己的范围来管理它们的生命周期.shared_ptr为此使用s特别有用,因为它简化了破坏,但你不应该将它们分发出来,因为那样你就会放弃你所寻求的关于它们释放的决定论.

处理#2

这是需要解决的问题.更详细地描述问题,则需要客户端接收它的行为像一个对象Subsystem,而Subsystem(和它的母公司System)存在,但表现一个后适当地Subsystem被破坏.

通过代理模式,状态模式和空对象模式的组合可以很容易地解决这个问题.虽然这似乎有点复杂的解决方案," 复杂性的另一方面只有简单性." 作为图书馆/ API开发人员,我们必须加倍努力,使我们的系统更加健壮.此外,我们希望我们的系统能够像用户期望的那样直观地行动,并在他们试图滥用它们时优雅地衰减.对于这个问题,很多解决方案,但是,这应该让你所有重要的点,你和斯科特·迈尔斯说,它是" 易于正确使用,而难以错误使用. "

现在,我假设在现实中,System在一些基类中进行交易,Subsystem从中得出各种不同的Subsystems.我在下面介绍了它SubsystemBase.您需要在下面引入一个Proxy对象,该对象通过将请求转发到它所代理的对象来SubsystemProxy实现接口SubsystemBase.(在这个意义上,它非常类似于Decorator模式的特殊用途应用程序.)每个Subsystem创建一个这样的对象,它通过a保存shared_ptr,并在请求via时返回GetProxy(),在调用时由父System对象GetSubsystem()调用.

当一个System超出范围时,它的每个Subsystem对象都会被破坏.在他们的析构函数中,他们调用mProxy->Nullify(),这导致他们的Proxy对象改变他们的状态.他们通过更改为指向实现接口的Null对象来执行此SubsystemBase操作,但这样做无需执行任何操作.

在此处使用状态模式允许客户端应用程序完全忘记特定是否Subsystem存在.而且,它不需要检查指针或保留应该被销毁的实例.

代理模式允许客户端依赖完全包装了该API的内部工作的详细情况的重量轻的物体上,并保持恒定的,统一的接口.

空对象模式允许代理原件后的功能Subsystem已被删除.

示例代码

我在这里放置了一个粗略的伪代码质量示例,但我对此并不满意.我把它重写为一个精确的,编译(我用g ++)我上面描述的例子.为了使它工作,我不得不介绍一些其他类,但他们的用途应该从他们的名字清楚.我在课堂上使用了Singleton模式NullSubsystem,因为它不需要多于一个. ProxyableSubsystemBase完全抽象出代理行为远离Subsystem它,允许它对这种行为一无所知.这是类的UML图:

子系统和系统层次结构的UML图

示例代码:

#include 
#include 
#include 

#include 


// 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 GetProxy() { return mProxy; }

  private:
    boost::shared_ptr 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 << "  : " << 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 > 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 is constructing.
  : Creating... Frank
  : Adding Subsystem:
  : Creating... Ernest
  : Adding Subsystem:
  main(): Assigning Subsystems to H1 and H2.
  main(): Doing something on H1 and H2.
  : Frank is DoingSomething (tm).
  : Ernest is DoingSomething (tm).
  main(): Leaving scope for System.
  : System is going out of scope.
  : Destroying... Frank
  : 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的一本书中读到的一篇有趣的文章谈到了使用Null Objects进行调试和开发.他们特别谈论使用Null Graphics Models和Textures,例如棋盘格纹,以使缺失的模型真正脱颖而出.通过更改NullSubsystemfor ReportingSubsystem会记录调用以及可能的callstack,只要访问它就可以应用同样的方法.这将允许您或您的图书馆的客户根据超出范围的内容追踪他们的位置,但不需要导致崩溃.

我在@Arkadiy评论中提到,他之间提出的循环依赖System并且Subsystem有点不愉快.通过SystemSubsystem依赖的接口派生出来,罗伯特C马丁的依赖倒置原则的应用可以很容易地弥补它.更好的做法是将Subsystem需要的功能与其父进程隔离,为其编写接口,然后保留该接口的实现者System并将其传递给Subsystems,这将通过a保存它shared_ptr.例如,你可能有LoggerInterface,你Subsystem用它写入日志,然后你可以从中派生CoutLoggerFileLogger从中,并保留这样的实例System.
消除循环依赖



2> Brian..:

这可以通过正确使用weak_ptr该类来实现.事实上,你已经非常接近于拥有一个好的解决方案.你是对的,你不能期望"超出想象"你的客户程序员,也不应该期望他们总是遵循你的API的"规则"(因为我相信你已经知道).所以,你真正做的最好的就是破坏控制.

我建议您调用GetSubsystem返回weak_ptr而不是shared_ptr简单,以便客户端开发人员可以测试指针的有效性,而无需始终声明对它的引用.

类似地,它pParentSystem是一个boost::weak_ptr可以内部检测其父级是否System仍然存在通过调用lockon pParentSystem以及检查NULL(原始指针不会告诉你这一点).

假设您将Subsystem类更改为始终检查其对应的System对象是否存在,您可以确保如果客户端程序员尝试使用Subsystem预期范围之外的对象(将由您控制),而不是一个无法解释的异常(您必须信任客户端程序员捕获/正确处理).

所以,在你的例子里main(),事情不会是蠢货!在Subsystemdtor中处理这个问题的最优雅的方法是让它看起来像这样:

class Subsystem
{
...
  ~Subsystem() {
       boost::shared_ptr 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)
  }
...
};

我希望这有帮助!

推荐阅读
郑小蒜9299_941611_G
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有