views:

1045

answers:

9

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

+1  A: 

In your example, it would be better if the System held a vector<Subsystem> rather than a vector<shared_ptr<Subsystem> >. Its both simpler, and eliminates the concern you have. GetSubsystem would return a reference instead.

Greg Rogers
It does not really eliminates his concerns. If a reference to Subsystem lives longer than the System, it will still end up pointing to an invalid object.
Nemanja Trifunovic
I actually had this in my solution originally, but it turns out it is actually simpler to have the `shared_ptr`s, as the destructor is then trivial.
Aaron
+2  A: 

I don't see a problem with having System::GetSubsystem return a raw pointer (RATHER than a smart pointer) to a Subsystem. Since the client is not responsible for constructing the objects, then there is no implicit contract for the client to be responsible for the cleanup. And since it is an internal reference, it should be reasonable to assume that the lifetime of the Subsystem object is dependent on the lifetime of the System object. You should then reinforce this implied contract with documentation stating as much.

The point is, that you are not reassigning or sharing ownership - so why use a smart pointer?

Jeremy Brown
This effectively means that we should get our hands "dirty" and delete Subsystems ourselves inside the destructor, which makes me feel bad about myself. Have I become a shared_ptr fanatic? Is it morally fine to mix shared_ptrs and raw pointers in a project? ;)
Josh
It would seem that is the case :-). Besides the actual functionality, the purpose of shared_ptr is to disambiguate ownership semantics. Your example seems to muddle it further. Another post suggests returning references instead of pointers (further disambiguation) - that's a style choice.
Jeremy Brown
You don't need to use raw pointers - that's why weak_ptr is there. Quoting from boost web site "Non-owning observers of an object owned by shared_ptr"
On Freund
Already up-modded the weak_ptr answer :-). That certainly answers the question of managing an indeterminate object lifetime scenario. However, Josh's yearning for construction/destruction sequences (the client would NEED to be aware) would suggest that weak_ptr could be overkill in this situation.
Jeremy Brown
The problem with returning a raw pointer is the pointer is left dangling when the Subsystem goes out of scope. The implicit contract is to not leave the client in a bad state, if you can help it. Documenting a known issue is almost as bad as writing it. If it's broke, fix it, don't describe it. =D
Aaron
Hehe, we could have a LONG discussion about the differing philosophies on idiot-proofing. Like I said, the weak_ptr solution is great when object lifetime is indeterminate. However, having dangling references in a synchronized scenario is indicative of a problem on the client side. (ctd...)
Jeremy Brown
If System is prematurely dead, then how do you trust the state on the client side? I don't think the question presents a problem complex enough to warrant a solution like yours. My impression from the OP is that he just wants things to automagically tidy up to avoid (icky) delete statements.
Jeremy Brown
He asks specifically for a solution that is "easy to use correctly and hard to use incorrectly." Raw pointers may be easy to use right (arguably), but they are also easy to use wrong. It comes down to a question of who you want to make it easy for, and how much rope you want to give them. =)
Aaron
Ah, but there's nothing inherently wrong with rope :-). If you can't trust your programmers that much, then I would argue that C++ is the wrong language for the project. Robustness gives diminishing returns for smaller projects. You have my vote for ansync/concurrent/network projects though!
Jeremy Brown
Fair enough. =D"To each his own, said the farmer as he kissed the cow."I develop libraries in C++ for a living (which are asynchronous, concurrent, and network based, strangely..), so my perspective is a bit skewed toward keeping my users from breaking themselves. My work is never done. =/
Aaron
+3  A: 

Here System cleearly owns the subsystems and I see no point in having shared ownership. I would simply return a raw pointer. If a Subsystem outlives its System, that's an error on its own.

Nemanja Trifunovic
+8  A: 

This is do-able with proper use of the weak_ptr class. In fact, you are already quite close to having a good solution. You are right that you cannot be expected to "out-think" your client programmers, nor should you expect that they will always follow the "rules" of your API (as I'm sure you are already aware). So, the best you can really do is damage control.

I recommend having your call to GetSubsystem return a weak_ptr rather than a shared_ptr simply so that the client developer can test the validity of the pointer without always claiming a reference to it.

Similarly, have pParentSystem be a boost::weak_ptr<System> so that it can internally detect whether its parent System still exists via a call to lock on pParentSystem along with a check for NULL (a raw pointer won't tell you this).

Assuming you change your Subsystem class to always check whether or not its corresponding System object exists, you can ensure that if the client programmer attempts to use the Subsystem object outside of the intended scope that an error will result (that you control), rather than an inexplicable exception (that you must trust the client programmer to catch/properly handle).

So, in your example with main(), things won't go BOOM! The most graceful way to handle this in the Subsystem's dtor would be to have it look something like this:

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

I hope this helps!

Brian
Up-voted Aaron's reply. My solution is quick-and-dirty, but his is more thorough.
Brian
Thank you. I tried to make it as exhaustive as possible.
Aaron
+21  A: 
Aaron
I really appreciate the amount of time and effort you were willing to put into your answer. Many thanks Aaron!
Josh
Great solution, Aaron!
Brian
+1  A: 

Stack objects will be released in the opposite order to which they where instantiated, so unless the developer using the API is trying to manage the smart pointer, it's normally not going to be a problem. There are just some things you are not going to be able to prevent, the best you can do is provide warnings at run time, preferably debug only.

Your example seems very much like COM to me, you have reference counting on the subsystems being returned by using shared_ptr, but you are missing it on the system object itself.

If each of the subsystem objects did an addref on the system object on creation, and a release on destruction you could at least display an exception if the reference count was incorrect when the system object is destroyed early.

Use of weak_ptr would also allow you to provide a message instead/aswell as blowing up when things are freed in the wrong order.

Greg Domjan
A: 

The essence of your problem is a circular reference: System refers to Subsystem, and Subsystem, in turn, refers to System. This kind of data structure cannot be easily handled by reference counting - it requires proper garbage collection. You're trying to break the loop by using a raw pointer for one of the edges - this will only produce more complications.

At least two good solutions have been suggested, so I will not attempt to outdo the previous posters. I can only note that in @Aaron's solution you can have a proxy for the System rather than for Subsystems - dependingo n what is more complex and what makes sense.

Arkadiy
The circular reference is certainly unpleasant, but I don't feel that it's part of the actual problem. Ideally, System would implement an interface that Subsystem would use (DIP), which eliminates the cycle, but that still leaves the issue of ownership being passed outside the hierarchy.
Aaron
The problem is that the ref counting is not sufficient for this situation. And that is caused by circular reference. If not for circularity, passing of ownership could have been handled with ref counting just fine.
Arkadiy
A: 

The real problem here is your design. There is no nice solution, because the model doesn't reflect good design principles. Here's a handy rule of thumb I use:

  • If an object holds a collection of other objects, and can return any arbitrary object from that collection, then remove that object from your design.

I realise that your example is contrived, but its an anti-pattern I see a lot at work. Ask yourself, what value is System adding that std::vector< shared_ptr<SubSystem> > doesnt? Users of your API need to know the interface of SubSystem (since you return them), so writing a holder for them is only adding complexity. At least people know the interface to std::vector, forcing them to remember GetSubsystem() above at() or operator[] is just mean.

Your question is about managing object lifetimes, but once you start handing out objects, you either lose control of the lifetime by allowing others to keep them alive (shared_ptr) or risk crashes if they are used after they have gone away (raw pointers). In multi-threaded apps its even worse - who locks the objects you are handing out to different threads? Boosts shared and weak pointers are a complexity inducing trap when used in this fashion, especially since they are just thread safe enough to trip up inexperienced developers.

If you are going to create a holder, it needs to hide complexity from your users and relieve them of burdens you can manage yourself. As an example, an interface consisting of a) Send command to subsystem (e.g. a URI - /system/subsystem/command?param=value) and b) iterate subsystems and subsystem commands (via an stl-like iterator) and possibly c) register subsystem would allow you to hide almost all of the details of your implementation from your users, and enforce the lifetime/ordering/locking requirements internally.

An iteratable/enumerable API is vastly preferable to exposing objects in any case - the commands/registrations could be easily serialised for generating test cases or configuration files, and they could be displayed interactively (say, in a tree control, with dialogs composed by querying the available actions/parameters). You would also be protecting your API users from internal changes you may need to make to the subsystem classes.

I would caution you against following the advice in Aarons answer. Designing a solution to an issue this simple that requires 5 different design patterns to implement can only mean that the wrong problem is being solved. I'm also weary of anyone who quotes Mr Myers in relation to design, since by his own admission:

"I have not written production software in over 20 years, and I have never written production software in C++. Nope, not ever. Furthermore, I’ve never even tried to write production software in C++, so not only am I not a real C++ developer, I’m not even a wannabe. Counterbalancing this slightly is the fact that I did write research software in C++ during my graduate school years (1985-1993), but even that was small (a few thousand lines) single-developer to-be-thrown-away-quickly stuff. And since striking out as a consultant over a dozen years ago, my C++ programming has been limited to toy “let’s see how this works” (or, sometimes, “let’s see how many compilers this breaks”) programs, typically programs that fit in a single file".

Not to say that his books aren't worth reading, but he has no authority to speak on design or complexity.

Jon
Ironically, I've just started to revisit my design again! I was googling around as I came across this thread and your new post. When I first made that post, I was so pissed off with smart pointers and their indeterministic behavior that I totally re-architected some 90K lines of code and removed all instances of shared_ptrs. That was a pain in the ass.After six months of using raw pointers, I'm in the same spot again! Raw pointers have their own problems. Managing shared objects is particularly hard. So I've been down both paths now and I know as a fact that each of them has pros and cons...
Josh
So my advice to anyone who's having the same problem is simple: don't be anal! I learned it through bitter experience that there's no one-fits-all solution. Think about ownership and think about it real well. Decide accordingly afterwards.And regarding your solution Jon, I've been down that path too. First of all, replacing System with a vector of Subsystems sacrifices encapsulation. What if you decide to roll up your own vector class and use that instead? Or switch to std::list all together? Some code might depend on the fact that vector's iterators are random-access for instance...
Josh
On the other hand, if you decide to hide too much of the internals, you'd end up with big monolithic classes that breach the Single Responsibility Principle and are hard to maintain. Copying all or most of Subsystem's interface to System's (out of the fear that giving Subsystem out as an internal is harmful) is just a bad idea. I think we are a generation of developers brought up by idealists, OOP fanatics, pattern Nazis and (to put it in Joel Spolsky's words) architecture astronauts. We are just too anal. Again, my best advice is to avoid the extremes.
Josh
@Jon. RE: "... requires 5 different design patterns ..." et al.Design Patterns are a language for describing ideas; they are used to convey a common idea without repeating its description every time you use it. I suppose you don't believe in using functions in code, which serve much the same purpose?
Aaron
@Jon. RE: "... weary of anyone who quotes Mr Myers in relation to design ..."I hardly think that "easy to use correctly and hard to use incorrectly." is a statement that is falsified by the admission of the utterer at lacking a particular knowledge in implementing said in a specific language. Unless you believe you can only design software in C++?
Aaron
It is interesting to pull quotations out of context and use them to make a point. Context is sometimes very useful, but not to those misusing such a quote. I often consider a quote to be invalid if it does not give a reference. The internet being what it is, it is not terribly difficult to track down the source of the Soctt Meyers quote you used. Here it is: http://www.artima.com/cppsource/top_cpp_books.html
Aaron
Further, I'll pull out my own out of context quote: "Given that I don’t really use C++, nor do I help specify it, you might wonder what I do do. Fundamentally, I study C++ and its application. I gather as much information as I can about the language and its use (from books, magazines, newsgroups, email and face-to-face conversations with developers and members of the standardization committee, experiments with toy programs I write, etc.), organize and analyze what I find, then I package my findings in concentrated form (e.g., books, magazine articles, technical presentations, etc.)
Aaron
for consumption for people like you--people who do use the language. Your job is to employ C++ as a tool to write useful software. My job is to discover and package the information you need to best apply that tool."
Aaron
In particular, I think Scott Meyers could be considered to be just as valid as the Encyclopedia Britannica, which by your logic, would not be a valid source of information, simply because none of its authors caused any of the events contained within, or did the research on whichever subject you were reading about. I suppose if you don't agree with an idea, it is easier to attack the author than the idea. Largely the idea behind V for Vendetta.
Aaron
Aaron, I couldn't reference the quote as I was prohibited from adding a link as a new user; thanks for posting it. WRT Mr Myers, I feel my point is valid, and it is not ad hominem - I don't think quoting him in a design discussion adds weight to ones argument. In retrospect, that was unnecessary, though, and detracts from my point which is that your solution is overcomplicated and brings with it other problems (dependencies, as noted and whether swallowing user actions with proxies is a good idea). The weak_ptr solution is better in that regard.
Jon
My point was that a good solution to this problem is to change the problem. A collection that exists only to hand out its members isn't doing anything to decrease complexity. In this case, making available actions rather than objects/interfaces allows the container to manage its internal state and hide lifetime/dependency issues. If the actions are enumerable then coupling between the collection and its users is minimal and can potentially be made available to API clients in a variety of ways.
Jon
Josh, you make some valid points, particularly WRT 'anality'. I would only say that wrt to having GetElement() type functions, you lose the ability to use standard algorithms on your collections, which IMO is a much bigger maintenance burden from the start than changing your implementation later. My point about std::vector was that an object that serves the same purpose needs to have some rationale for being written (i.e. don't reinvent the wheel). A hand-rolled collection with a non-idiomatic interface has several strikes against even it before considering its contained objects lifetimes.
Jon
+1  A: 

You were right at the very beginning in your first paragraph. Your designs based on RAII (like mine and most well written C++ code) require that your objects are held by exclusive ownership pointers. In Boost that would be scoped_ptr.

So why didn't you use scoped_ptr. It will certainly be because you wanted the benefits of weak_ptr to protect against dangling references but you can only point a weak_ptr at a shared_ptr. So you have adopted the common practice of expediently declaring shared_ptr when what you really wanted was single ownership. This is a false declaration and as you say, it compromises destructors being called in the correct sequence. Of course if you never ever share the ownership you will get away with it - but you will have to constantly check all of your code to make sure it was never shared.

To make matters worse the boost::weak_ptr is inconvenient to use (it has no -> operator) so programmers avoid this inconvenience by falsely declaring passive observing references as shared_ptr. This of course shares ownership and if you forget to null that shared_ptr then your object will not get destroyed or its destructor called when you intend it to.

In short, you have been shafted by the boost library - it fails to embrace good C++ programming practices and forces programmers to make false declarations in order to try and derive some benefit from it. It is only useful for scripting glue code that genuinely wants shared ownership and is not interested in tight control over memory or destructors being called in the correct sequence.

I have been down the same path as you. Protection against dangling pointers is badly needed in C++ but the boost library does not provide an acceptable solution. I had to solve this problem - my software department wanted assurances that C++ can be made safe. So I rolled my own - it was quite a lot of work and can be found at:

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

It is totally adequate for single threaded work and I am about to update it to embrace pointers being shared across threads. Its key feature is that it supports smart (self-zeroing) passive observers of exclusively owned objects.

Unfortunately programmers have become seduced by garbage collection and 'one size fits all' smart pointer solutions and to a large extent are not even thinking about ownership and passive observers - as a result they do not even know that what they are doing is wrong and don't complain. Heresy against Boost is almost unheard of!

The solutions that have been suggested to you are absurdly complicated and of no help at all. They are examples of the absurdity that results from a cultural reluctance to recognize that object pointers have distinct roles that must be correctly declared and a blind faith that Boost must be the solution.

John Morrison
Thank you John, both for your answer and your magnificent smart pointer system. Very much appreciated.
Josh