Sign in to follow this  

To boost::shared_ptr or not to boost::shared_ptr

This topic is 3455 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Was wondering whether or not to base my game on shared_ptr to help reduce memory leaks and other bugs. I'm not creating Doom 4 but nor am I making a minesweeper game. Any reason to not use shared_ptr throughout the engine and game logic? I have revisited the idea since TR1 (with shared_ptr) is included in VS2008 SP1 beta. I will use boost implementation until SP1 is released.

Share this post


Link to post
Share on other sites
If you need shared ownership semantics, then by all means use it. It works very well for that (provided you are aware of circular referencing problems).

When working in C++, you can usually find a superior alternative to raw pointers most of the time. That alternative isn't necessarily shared_ptr, but its a common and useful tool.

I'm not sure what you mean by "base my game on shared_ptr". shared_ptr is a tool, an implementation detail. Use it only where appropriate.

Share this post


Link to post
Share on other sites
I've been reading up on TR1 for C++ and some have advocated to use the shared_ptr instead of raw pointer from now on unless speed is critical.

Microsoft programmer who is on the VC++ dev team, suggests we should use shared_ptr from now on. He likens it to using std::vector<MyObject> to creating an array manually MyObject[200]. Of course, edge cases could use manual optimization.

I've done "sizeof" on the smart pointer and it returns 8. Am I to believe that the smart pointer is therefore 8 bytes total? Which isn't a big deal going from a 4 byte pointer (32bit OS).

Would it be foolish to use shared_ptr from now on rather than raw pointers?

P.S. I'm not creating/destroying objects in tight loops within loops.

[EDIT] PSS. Of course I'm also talking about using them on object needed to reside on heap, stack objects will not use shared_ptr.

Share this post


Link to post
Share on other sites
Quote:
Original post by azherdev
I've done "sizeof" on the smart pointer and it returns 8. Am I to believe that the smart pointer is therefore 8 bytes total?


shared_ptr is pretty much guaranteed to be the size of two pointers. The first pointer points to your heap allocated object, and the second pointer points to a heap allocated reference count.

intrusive_ptr OTOH, is pretty much guaranteed to be exactly the size of one pointer.

I say "pretty much" in both cases because it is conceivable that padding could cause the objects to be larger, but that isn't the case on the compilers/cpus you're to likely encounter.

Quote:
Original post by azherdev
Which isn't a big deal going from a 4 byte pointer (32bit OS).


You're right. It isn't a big deal at all.

Quote:
Original post by azherdev
Would it be foolish to use shared_ptr from now on rather than raw pointers?


Off the top of my head I can think of one reason not to. For example shared_ptr's reference count is not thread safe. Though you can get around this by implementing a thread safe reference counted object that is compatible with intrusive_ptr.

Quote:
Original post by azherdev
PSS. Of course I'm also talking about using them on object needed to reside on heap, stack objects will not use shared_ptr.


You don't have to restrict your use of shared_ptr to managing objects on the heap. Consider this somewhat contrived example:


// Custom deleter function object
struct CustomDeleter
{
operator()(FILE* fp)
{
fclose(fp);
}
};

...

// Create a shared_ptr representing a resource
shared_ptr<FILE> myFileHandle( fopen("MyFile.txt", "r"), CustomDeleter());


In the above code we create a shared_ptr to automatically close our file handle when it is no longer needed. This is achieved through the use of a custom deleter function object.

Share this post


Link to post
Share on other sites
Reading the following link:

http://www.boost.org/doc/libs/1_35_0/libs/smart_ptr/shared_ptr.htm#ThreadSafety

I'm a bit confused. The undefined behavior that they talk about has to do with the object that the shared_ptr points to or the shared_ptr object itself? They mention something about atomic reference count.

I understand that if thread A reduces the counter to 0 and thread B tries to access the object while it is being destroyed by thread A. Is that the only problem or can the reference count in the shared_ptr actually screw up and either create a resource leak or prematurely reduce the reference count to 0?

Share this post


Link to post
Share on other sites
Quote:
Original post by azherdev
I'm a bit confused. The undefined behavior that they talk about has to do with the object that the shared_ptr points to or the shared_ptr object itself?

The shared_ptr object itself. shared_ptr has no way of making thread safety guarantees beyond itself.

Quote:
I understand that if thread A reduces the counter to 0 and thread B tries to access the object while it is being destroyed by thread A. Is that the only problem

No.

Quote:
or can the reference count in the shared_ptr actually screw up and either create a resource leak or prematurely reduce the reference count to 0?

Yes. If thread A reassigns shared_ptr P while thread B tries to copy P, chances are high that eventually you'll end up with a copy which points at one item but refers to the reference count of another item.

If you need to share a single shared_ptr instance between threads that's not immutable, you'd want to use something like boost::shared_mutex -- lock_shared()ing before reading and lock()ing (and exclusive lock) before writing.

Share this post


Link to post
Share on other sites
Quote:

I understand that if thread A reduces the counter to 0 and thread B tries to access the object while it is being destroyed by thread A. Is that the only problem or can the reference count in the shared_ptr actually screw up and either create a resource leak or prematurely reduce the reference count to 0?

This shouldn't really ever happen. Generally speaking there is no race condition for the reference counter as the increase and decrease operation is implemented in an atomic fashion. On Windows there's the InterlockedIncrement and InterlockedDecrement functions. I would be surprised if the boost shared_ptr doesn't use the interlocked variable access functions if you look at the detail files.

The reset function however probably isn't protected as it operates on the object pointer and on the reference counter pointer. This means that you would have to protect it with a critical section or similar heavier synchronization object which is probably outside of the lightness requirement of the shared_ptr. I haven't looked at it though so it's just speculation.

Share this post


Link to post
Share on other sites
I use the Boost.Pointers library exclusively in my projects and it has been very, very helpful. (However, neither is multithreaded, so I never had to deal with any of those tough issues.)

I actually use a scheme where every class I want to have "managed" has a set of pointers generated for it in a file I usually name pointers.hpp. My game engine, Mocha, defines macros for this that child projects also use in their own pointers.hpp file.

This is straight from my repository.


#ifndef MOCHA_POINTERS_HEADER
#define MOCHA_POINTERS_HEADER

/**
* @file Contains forward declarartions and pointer type declarations for commonly used objects, particularly reference counted objects. Types are created of the form *_ptr_t and const_*_ptr_t. Additionally, helper function to create new pointers are created of the form new_*().
*/


#include <boost/shared_ptr.hpp>

#define MOCHA_PTR_DECL(X) class X ; typedef X * X##_ptr_t; typedef const X * const_##X##_ptr_t;
#define MOCHA_NAMED_PTR_DECL(X,Y) class X ; typedef X * Y##_ptr_t; typedef const X * const_##Y##_ptr_t;
#define MOCHA_SHARED_PTR_DECL(X) class X ; typedef boost::shared_ptr< X > X##_ptr_t; typedef boost::shared_ptr<const X > const_##X##_ptr_t; typedef boost::weak_ptr< X > X##_wptr_t; typedef boost::weak_ptr<const X > const_##X##_wptr_t;
#define MOCHA_NAMED_SHARED_PTR_DECL(X,Y) class X ; typedef boost::shared_ptr< X > Y##_ptr_t; typedef boost::shared_ptr<const X > const_##Y##_ptr_t; typedef boost::weak_ptr< X > Y##_wptr_t; typedef boost::weak_ptr<const X > const_##Y##_wptr_t;
#define MOCHA_PTR_NEW_DECL(X) X##_ptr_t new_##X () { return new X (); }
#define MOCHA_SHARED_PTR_NEW_DECL(X) inline X##_ptr_t new_##X () { return X##_ptr_t(new X ()); }

namespace mocha {
MOCHA_SHARED_PTR_DECL(kernel)
MOCHA_SHARED_PTR_DECL(module)
namespace events {
MOCHA_SHARED_PTR_DECL(event_queue)
MOCHA_SHARED_PTR_DECL(event_queue_impl)
}
namespace graphics {
MOCHA_SHARED_PTR_DECL(basic_vertex_buffer)
MOCHA_NAMED_SHARED_PTR_DECL(basic_vertex_buffer, vertex_buffer)
MOCHA_SHARED_PTR_DECL(bitmap_font)
MOCHA_SHARED_PTR_DECL(bitmap_text)
MOCHA_SHARED_PTR_DECL(graphics)
MOCHA_SHARED_PTR_DECL(graphics_impl)
MOCHA_SHARED_PTR_DECL(image)
MOCHA_SHARED_PTR_DECL(job_buffer)
MOCHA_SHARED_PTR_DECL(paintable)
MOCHA_SHARED_PTR_DECL(paint_job)
MOCHA_SHARED_PTR_DECL(raster)
MOCHA_SHARED_PTR_DECL(raster_sprite)
MOCHA_SHARED_PTR_DECL(scene_node)
MOCHA_SHARED_PTR_DECL(sequence)
MOCHA_SHARED_PTR_DECL(texture)
MOCHA_SHARED_PTR_DECL(textured_vertex_buffer)
MOCHA_SHARED_PTR_DECL(vertex_buffer_base)
}
namespace input {
MOCHA_SHARED_PTR_DECL(input)
MOCHA_SHARED_PTR_DECL(input_impl)
MOCHA_SHARED_PTR_DECL(joystick)
}
namespace network {
MOCHA_SHARED_PTR_DECL(network)
MOCHA_SHARED_PTR_DECL(network_impl)
MOCHA_SHARED_PTR_DECL(socket)
}
namespace timing {
MOCHA_SHARED_PTR_DECL(timer)
MOCHA_SHARED_PTR_DECL(timer_impl)
}
}

#endif






The macros that include SHARED in their name create a series of typedefs based on boost::shared_ptr and boost::weak_ptr (both are needed, because circular dependencies need to be kept in mind). The naming convention is very convenient, and other projects of mine take advantage of the other macros that use plain old pointers for consistent naming.

For a class foo, the typedefs created are foo_ptr_t, foo_wptr_t, const_foo_ptr_t, and const_foo_wptr_t. I'm considering having the weak pointer typedefs renamed to yield foo_weak_ptr_t, since the 'w' in the current convention is very easy to miss.

For other "non-managed" types, I use a different macro, but get the same naming convention. For a "manual" class bar, the typedefs bar_ptr_t and const_bar_ptr_t are generated (note that there are no weak pointer types in this case).

Anyway, this has worked very well for me thus far and is a very important part of resource management in my system as many resources are (nearly) "self managing". In the end, client code is much simpler and less error prone.

In short, I'd go for it. Just my two cents. :-) I dunno if this was helpful, but I thought I'd share.

Share this post


Link to post
Share on other sites
IMO shared_ptr is pure evil and should not be used.
It is only to manage shared ownership, which shouldn't even happen in a normal program, unless as a low-level implicit sharing optimization.
A good C++ program shouldn't use pointers at all, but only values and weak references.

Share this post


Link to post
Share on other sites
Quote:

IMO shared_ptr is pure evil and should not be used.
It is only to manage shared ownership, which shouldn't even happen in a normal program, unless as a low-level implicit sharing optimization.
A good C++ program shouldn't use pointers at all, but only values and weak references.

This is wrong. The semantics of the C++ reference qualifier do not allow some types of important referential semantics (null, reseatable) that are required to implement many useful constructs in a sane fashion (such as linked lists). Values are not suitable any place referential semantics are required.

C++'s pointer mechanism isn't necessarily the best way to acquire said referential semantics, but abstractions on top if -- like shared_ptr and (in appropriate cases) auto_ptr and the like are excellent.

Share this post


Link to post
Share on other sites
Quote:
Original post by asp_
Quote:

I understand that if thread A reduces the counter to 0 and thread B tries to access the object while it is being destroyed by thread A. Is that the only problem or can the reference count in the shared_ptr actually screw up and either create a resource leak or prematurely reduce the reference count to 0?

This shouldn't really ever happen. Generally speaking there is no race condition for the reference counter as the increase and decrease operation is implemented in an atomic fashion.


Says who?

There is no requirement that the reference count be implemented in an atomic fashion. For example the implementation of boost::shared_pointer explicitly doesn't synchronize the reference count. What reason do you have to believe that std::tr1::shared_ptr does?

Since boost::shared_ptr and std::tr1::shared_ptr are implementations of the same specification (see: TR1), we can say with certainty that atomicity is not a requirement.

As I said before, you can overcome this by simply using boost::intrusive_ptr and implementing your own synchronized reference counter.

Quote:
Original post by asp_
The reset function however probably isn't protected as it operates on the object pointer and on the reference counter pointer.


It would be sufficient to synchronize the reference count alone.

Share this post


Link to post
Share on other sites
Quote:
Original post by fpsgamer

// Custom deleter function object
struct CustomDeleter
{
operator()(FILE* fp)
{
fclose(fp);
}
};

...

// Create a shared_ptr representing a resource
shared_ptr<FILE> myFileHandle( fopen("MyFile.txt", "r"), CustomDeleter());



You don't even need to go as far as that;

shared_ptr<FILE> file(fopen("MyFile.txt", "r"), fclose);

I've used that before when having to deal with FILE* and libpng to avoid worrying about clean up [smile]


Share this post


Link to post
Share on other sites
Quote:

There is no requirement that the reference count be implemented in an atomic fashion. For example the implementation of boost::shared_pointer explicitly doesn't synchronize the reference count. What reason do you have to believe that std::tr1::shared_ptr does?

I never said anything about TR1. I specifically said to examine the detail space of boost. Just because you couldn't look it up yourself I've done it for you:


boost/shared_ptr.hpp
member of shared_ptr
boost::detail::shared_count pn; // reference counter



boost/detail/shared_count.hpp
member of shared_count
sp_counted_base * pi_;



boost/detail/sp_counted_base.hpp
includes sp_counted_base_w32.hpp



boost/detail/sp_counted_base_w32.hpp
member of sp_counted_base
long use_count_; // #shared
long weak_count_; // #weak + (#shared != 0)

void release() // nothrow
{
if( BOOST_INTERLOCKED_DECREMENT( &use_count_ ) == 0 )
{
dispose();
weak_release();
}
}




boost/detail/interlocked.hpp
# include <windows.h>

# define BOOST_INTERLOCKED_INCREMENT InterlockedIncrement


Oh my god. And look the GCC version does it as well.. and every other version... Do you realize how completely useless the shared_ptr would be without atomic reference count updates? Asynchronous operations would be completely impossible under more than one thread. Also synchronizing the count alone would clearly not be sufficient when you have a reset function, the document that in the example you posted.

Share this post


Link to post
Share on other sites
Woah guys, no need to fight! :)

Thank you for your input and I do have a better understanding now. I like raw pointers, but if I can get used to using shared_ptr (in mt environment) and not be penalized with speed much, I think I'll be switching over.

Share this post


Link to post
Share on other sites
Quote:
Original post by loufoque
shared ownership, which shouldn't even happen in a normal program, unless as a low-level implicit sharing optimization.


This is some kind of joke, right? You are aware, I hope, that many *very high* level languages share objects *all over the place, automatically*? It's called "reference semantics", and they're very, very useful. Of course, built-in garbage collection helps a lot with that kind of system :)

Share this post


Link to post
Share on other sites
Quote:
Original post by asp_
I never said anything about TR1.


The OP is specifically referring to TR1. Thats why I mentioned it.

Quote:
Original post by asp_
I specifically said to examine the detail space of boost. Just because you couldn't look it up yourself I've done it for you:


Thats needlessly obnoxious, and yes I did look it up.

You've unfortunately misunderstood the synchronization guarantees made by boost::shared_ptr. boost::shared_ptr is distinctly lock-free as evidenced by the following quote:

"Starting with Boost release 1.33.0, shared_ptr uses a lock-free implementation on the following platforms:

* GNU GCC on x86 or x86-64;
* GNU GCC on IA64;
* Metrowerks CodeWarrior on PowerPC;
* GNU GCC on PowerPC;
* Windows."


The problem is that the synchronization methods employed do not provide mutual exclusion for both reads and writes.

Quote:
Original post by asp_
Oh my god. And look the GCC version does it as well.. and every other version... Do you realize how completely useless the shared_ptr would be without atomic reference count updates? Asynchronous operations would be completely impossible under more than one thread.


In spite of lack of mutual exclusion, you can use shared_ptr on multiple threads under certain conditions:

  • shared_ptr objects offer the same level of thread safety as built-in types. A shared_ptr instance can be "read" (accessed using only const operations) simultaneously by multiple threads.


  • Different shared_ptr instances can be "written to" (accessed using mutable operations such as operator= or reset) simultaneosly by multiple threads (even when these instances are copies, and share the same reference count underneath.)


  • Any other simultaneous accesses result in undefined behavior

eg.

//--- Example 4 ---

// thread A
p3 = p2; // reads p2, writes p3

// thread B
// p2 goes out of scope: undefined, the destructor is considered a "write access"



If you read the documentation, things will be clear. And for the last time: if you need synchronized reference counts use intrusive_ptr.

Share this post


Link to post
Share on other sites
Any reference count synchronization problems with shared_ptr can easily be overcome by giving each thread its own shared_ptr instance. That way you're practically guaranteed that the reference count is always correct and the d'tor of the shared object is only run once and only by one thread.

Share this post


Link to post
Share on other sites
Quote:
Original post by Red Ant
Any reference count synchronization problems with shared_ptr can easily be overcome by giving each thread its own shared_ptr instance. That way you're practically guaranteed that the reference count is always correct and the d'tor of the shared object is only run once and only by one thread.


Not quite. Counter example:

Suppose you have three threads, the main thread, thread A and thread B.

The main thread has the original shared_ptr. Threads A and B have copies of that shared_ptr.

Lets say thread A passes the shared_ptr to another function by value (read/write access) when thread B terminates (write access).

Voila, undefined behavior.

Share this post


Link to post
Share on other sites
Quote:
Original post by loufoque
IMO shared_ptr is pure evil and should not be used.
It is only to manage shared ownership, which shouldn't even happen in a normal program, unless as a low-level implicit sharing optimization.
A good C++ program shouldn't use pointers at all, but only values and weak references.


Wrong on multiple levels.

1) Pointers are a necessary component in the implementation of containers, and while the standard library ones often suffice, there are times when a custom implementation is required.

2) Further, the nature of C++'s references are such that they cannot be reseated, pointers are acceptable as a reseatable alternative in the case that this is necessary.

3) Unlike builtin constructs, shared_ptr, when used in conjunction with weak_ptr, infers much greater safety when it comes to weak references, allowing you to have defined behavior when the rug is pulled out from underneath some part of the program.

4) While clearly defined ownership is all fine and good, shared_ptr is great for the situations where clearly defined ownership is either inappropriate or costly in implementation and mantinence (which the principle of KISS says is inappropriate, getting back to the original point).

Share this post


Link to post
Share on other sites
Quote:
Original post by fpsgamer
Lets say thread A passes the shared_ptr to another function by value (read/write access) when thread B terminates (write access).

Voila, undefined behavior.


I don't understand. How's that undefined? Thread A essentially copy constructs a new shared_ptr instance from an existing one while thread B runs the d'tor of its shared_ptr instance. All accesses to the underlying reference count are properly synchronized, and when all is said and done the reference count is still going to be consistent.

Share this post


Link to post
Share on other sites
Quote:
Original post by Red Ant
All accesses to the underlying reference count are properly synchronized.


I've said this a couple of times already and its in the documentation:

Reference counts are not synchronized for simultaneous reads and writes.

There may be multiple readers of the reference count.
There may be multiple writers of the reference count.
There may not be simultaneous reads and writes of the reference count.

If your reference count requires mutual exclusion use intrusive_ptr.

Share this post


Link to post
Share on other sites
Quote:
Original post by fpsgamer
I've said this several time already and its in the documentation:

Reference counts are not synchronized for simultaneous reads and writes.

There may be multiple readers of the reference count.
There may be multiple writers of the reference count.
There may not be simultaneous reads and writes of the reference count.

If your reference count requires mutual exclusion use intrusive_ptr.



I have read the documentation ... in fact I'm reading it again right now, and I can't find anywhere where it says that.

EDIT: Can anybody else confirm fpsgamer's claim?

Share this post


Link to post
Share on other sites
Quote:
Original post by Red Ant
Quote:
Original post by fpsgamer
I've said this several time already and its in the documentation:

Reference counts are not synchronized for simultaneous reads and writes.

There may be multiple readers of the reference count.
There may be multiple writers of the reference count.
There may not be simultaneous reads and writes of the reference count.

If your reference count requires mutual exclusion use intrusive_ptr.



I have read the documentation ... in fact I'm reading it again right now, and I can't find anywhere where it says that.


Starting at the quote "Any other simultaneous accesses result in undefined behavior.", and read the examples that follow.

Share this post


Link to post
Share on other sites
The reference count is guarded by InterlockedIncrement and InterlockedDecrement which has to make them safe. InterlockedX guarantees atomicity. Either you're misunderstanding how this works or I'm completely missing what you're saying.

Why you would have references or pointers to shared_ptr's across different threads I have no idea.. you'd always have value types.

Quote:

A shared_ptr instance can be "read" (accessed using only const operations) simultaneously by multiple threads

This specifically says a shared_ptr instance. Which would require you to have a pointer or reference to a shared_ptr in order to reference the same instance. Maybe I'm misreading it but I can't see any other situation it would fail under.

Share this post


Link to post
Share on other sites
fpsgamer, none of the 5 examples do what you have described in your post. The mixed read/write operations that are flagged as "undefined" involve reading and writing THE SAME SHARED_PTR INSTANCE, but not reading and writing different shared_ptr's that reference the same object.

Share this post


Link to post
Share on other sites

This topic is 3455 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this