Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

A Question On Smart Pointers and Their Inevitable Indeterminism

I've been extensively using smart pointers (boost::shared_ptr to be exact) in my projects for the last two years. I understand and appreciate their benefits and I generally like them a lot. But the more I use them, the more I miss the deterministic behavior of C++ with regarding to memory management and RAII that I seem to like in a programming language. Smart pointers simplify the process of memory management and provide automatic garbage collection among other things, but the problem is that using automatic garbage collection in general and smart pointer specifically introduces some degree of indeterminisim in the order of (de)initializations. This indeterminism takes the control away from the programmers and, as I've come to realize lately, makes the job of designing and developing APIs, the usage of which is not completely known in advance at the time of development, annoyingly time-consuming because all usage patterns and corner cases must be well thought of.

To elaborate more, I'm currently developing an API. Parts of this API requires certain objects to be initialized before or destroyed after other objects. Put another way, the order of (de)initialization is important at times. To give you a simple example, let's say we have a class called System. A System provides some basic functionality (logging in our example) and holds a number of Subsystems via smart pointers.

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

As you can already tell, a Subsystem is only meaningful in the context of a System. But a Subsystem in such a design can easily outlive its parent System.

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

If we had used raw pointers to hold subsystems, we would have destroyed subsystems when our system had gone down, of course then, pSomeSubsystem would be a dangling pointer.

Although, it's not the job of an API designer to protect the client programmers from themselves, it's a good idea to make the API easy to use correctly and hard to use incorrectly. So I'm asking you guys. What do you think? How should I alleviate this problem? How would you design such a system?

Thanks in advance, Josh

like image 256
Ash Avatar asked Dec 30 '08 18:12

Ash


People also ask

What is a smart pointer why it is used and describe the types?

A Smart Pointer is a wrapper class over a pointer with an operator like * and -> overloaded. The objects of the smart pointer class look like normal pointers. But, unlike Normal Pointers it can deallocate and free destroyed object memory.

What is smart pointer when should we Use it?

Reference-counted smart pointer. Use when you want to assign one raw pointer to multiple owners, for example, when you return a copy of a pointer from a container but want to keep the original. The raw pointer is not deleted until all shared_ptr owners have gone out of scope or have otherwise given up ownership.

What is super pointer?

SUPER Pointer. Overview. For each function block that extends a base function block, a pointer with name SUPER is automatically available. It points to the base function block instances.

What is smart pointer in c++ 11?

Concept of the C++11 Smart Pointers Smart pointers are class objects that behave like built-in pointers but also manage objects that you create with new so that you don't have to worry about when and whether to delete them - the smart pointers automatically delete the managed object for you at the appropriate time.


1 Answers

Problem Summary

There are two competing concerns in this question.

  1. Life-cycle management of Subsystems, allowing their removal at the right time.
  2. Clients of Subsystems need to know that the Subsystem they are using is valid.

Handling #1

System owns the Subsystems and should manage their life-cycle with it's own scope. Using shared_ptrs for this is particularly useful as it simplifies destruction, but you should not be handing them out because then you loose the determinism you are seeking with regard to their deallocation.

Handling #2

This is the more intersting concern to address. Describing the problem in more detail, you need clients to receive an object which behaves like a Subsystem while that Subsystem (and it's parent System) exists, but behaves appropriately after a Subsystem is destroyed.

This is easily solved by a combination of the Proxy Pattern, the State Pattern and the Null Object Pattern. While this may seem to be a bit complex of a solution, 'There is a simplicity only to be had on the other side of complexity.' As Library/API developers, we must go the extra mile to make our systems robust. Further, we want our systems to behave intuitively as a user expects, and to decay gracefully when they attempt to misuse them. There are many solutions to this problem, however, this one should get you to that all important point where, as you and Scott Meyers say, it is "easy to use correctly and hard to use incorrectly.'

Now, I am assuming that in reality, System deals in some base class of Subsystems, from which you derive various different Subsystems. I've introduced it below as SubsystemBase. You need to introduce a Proxy object, SubsystemProxy below, which implements the interface of SubsystemBase by forwarding requests to the object it is proxying. (In this sense, it is very much like a special purpose application of the Decorator Pattern.) Each Subsystem creates one of these objects, which it holds via a shared_ptr, and returns when requested via GetProxy(), which is called by the parent System object when GetSubsystem() is called.

When a System goes out of scope, each of it's Subsystem objects gets destructed. In their destructor, they call mProxy->Nullify(), which causes their Proxy objects to change their State. They do this by changing to point to a Null Object, which implements the SubsystemBase interface, but does so by doing nothing.

Using the State Pattern here has allowed the client application to be completely oblivious to whether or not a particular Subsystem exists. Moreover, it does not need to check pointers or keep around instances that should have been destroyed.

The Proxy Pattern allows the client to be dependent on a light weight object that completely wraps up the details of the API's inner workings, and maintains a constant, uniform interface.

The Null Object Pattern allows the Proxy to function after the original Subsystem has been removed.

Sample Code

I had placed a rough pseudo-code quality example here, but I wasn't satisfied with it. I've rewritten it to be a precise, compiling (I used g++) example of what I have described above. To get it to work, I had to introduce a few other classes, but their uses should be clear from their names. I employed the Singleton Pattern for the NullSubsystem class, as it makes sense that you wouldn't need more than one. ProxyableSubsystemBase completely abstracts the Proxying behavior away from the Subsystem, allowing it to be ignorant of this behavior. Here is the UML Diagram of the classes:

UML Diagram of Subsystem and System Hierarchy

Example Code:

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

Output from the code:

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.

Other Thoughts:

  • An interesting article I read in one of the Game Programming Gems books talks about using Null Objects for debugging and development. They were specifically talking about using Null Graphics Models and Textures, such as a checkerboard texture to make missing models really stand out. The same could be applied here by changing out the NullSubsystem for a ReportingSubsystem which would log the call and possibly the callstack whenever it is accessed. This would allow you or your library's clients to track down where they are depending on something that has gone out of scope, but without the need to cause a crash.

  • I mentioned in a comment @Arkadiy that the circular dependency he brought up between System and Subsystem is a bit unpleasant. It can easily be remedied by having System derive from an interface on which Subsystem depends, an application of Robert C Martin's Dependency Inversion Principle. Better still would be to isolate the functionality that Subsystems need from their parent, write an interface for that, then hold onto an implementor of that interface in System and pass it to the Subsystems, which would hold it via a shared_ptr. For example, you might have LoggerInterface, which your Subsystem uses to write to the log, then you could derive CoutLogger or FileLogger from it, and keep an instance of such in System.
    Eliminating the Circular Dependency

like image 142
Aaron Avatar answered Oct 31 '22 16:10

Aaron