
Hi Everybody, Smart pointers in boost work on a principle of preventing object destruction while references to it exist anywhere in the system. It causes that whoever owns a boost::shared_ptr to some object, has power to decide for how long will that object live. This is "ownership". In some situations it would be more convenient if object itself decided when to die (owned itself?), and notified all reference holders about this fact, so that they not try to use it any more. A real-life example from a computer game code. There are C++ objects representing tanks on the battlefield. When tank gets hit, it explodes and ceases to exist. The nice, straighforward way to do it is to write "delete this;" in C++. All references to deleted tank which are held by other tanks (for example those currently aiming at it, or following it), will be immediately updated to NULL, provided that they are live_ptrs: template<class T> class live_ptr { // Member functions closely resembling those of shared_ptr // When object pointed to is deleted, this pointer resets its value to NULL }; class Tank; void live_ptr_test() { Tank *tank = new Tank; live_ptr<Tank> p(tank); // not ownership, but a "live" reference delete tank; assert(p == 0); } I have been using (gradually improved) variations of such a smart pointer in some of my projects for last couple of years, and found it useful. Anybody interested? cheers, Marcin Kalicinski

"Marcin Kaliciñski" wrote:
Smart pointers in boost work on a principle of preventing object destruction while references to it exist anywhere in the system. It causes that whoever owns a boost::shared_ptr to some object, has power to decide for how long will that object live. This is "ownership".
In some situations it would be more convenient if object itself decided when to die (owned itself?), and notified all reference holders about this fact, so that they not try to use it any more.
Could you compare it to smart_ptr/weak_ptr combination? /Pavel

Marcin Kaliciñski wrote:
Hi Everybody,
Smart pointers in boost work on a principle of preventing object destruction while references to it exist anywhere in the system. It causes that whoever owns a boost::shared_ptr to some object, has power to decide for how long will that object live. This is "ownership". In some situations it would be more convenient if object itself decided when to die (owned itself?), and notified all reference holders about this fact, so that they not try to use it any more.
You can do this with shared_ptr/weak_ptr.
A real-life example from a computer game code. There are C++ objects representing tanks on the battlefield. When tank gets hit, it explodes and ceases to exist. The nice, straighforward way to do it is to write "delete this;" in C++. All references to deleted tank which are held by other tanks (for example those currently aiming at it, or following it), will be immediately updated to NULL, provided that they are live_ptrs: [...]
You can make the object own itself by adding a shared_ptr member to it that holds a self-reference (this_). The self-destruction is accomplished with this_.reset(). All weak_ptr instances will "expire". Or you can use a null deleter if you want to delete the object explicitly and not use shared_ptr to manage its lifetime: http://boost.org/libs/smart_ptr/sp_techniques.html#weak_without_shared but this solution has a drawback that I mention below. In practice, my tanks are usually owned by a container (vector< shared_ptr<Tank> > in the simplest case), because I need to enumerate them.
template<class T> class live_ptr { // Member functions closely resembling those of shared_ptr // When object pointed to is deleted, this pointer resets its value to NULL };
The problem with providing such an interface is lack of reentrancy and thread safety. Consider this code: live_ptr<X> px; if( px != 0 ) { px->f(); } If after the px != 0 check the object is deleted by a different thread, px->f() will crash. This can also happen in some complex single threaded code. weak_ptr::lock is designed to prevent this from happening by giving you a shared_ptr to the object which keeps it alive.
class Tank;
void live_ptr_test() { Tank *tank = new Tank; live_ptr<Tank> p(tank); // not ownership, but a "live" reference delete tank; assert(p == 0); }
Your live_ptr<> requires support from Tank, right?

You can do this with shared_ptr/weak_ptr.
You are right. I discovered that some time ago, but found this solution more cumbersome to use compared to the one I already had (live_ptr). With shared_ptr/weak_ptr you have to: - add a data member of type shared_ptr - remember to initialize it in all constructors - add way to obtain weak_ptrs from the stored shared_ptr - probably by adding an extra member function Last but not least, despite of all the optimization effort people put into these classes, shared_ptr/weak_ptr combination is quite slow for that task. I have run several tests with full optimization enabled in Visual Studio C++ 2003, P4 2.4GHz: [live_ptr | shared_ptr/weak_ptr] Single threaded: 0.921s | 1.891s (live_ptr is 2x faster) Multi threaded: 1.015s | 8.063s (live_ptr is 8x faster) Below is source code for that test: #include <iostream> #include <ctime> #include <vector> #include <boost/weak_ptr.hpp> #include <boost/shared_ptr.hpp> #include "live_ptr.hpp" using namespace std; using namespace boost; // Class for testing live_ptr performance class A: public live_ptr_target<A> { public: live_ptr<A> r1, r2, r3, r4; }; // Class for testing shared_ptr/weak_ptr performance class B { shared_ptr<B> sp; public: B() { sp.reset(this); } weak_ptr<B> get_ref() { return weak_ptr<B>(sp); } void destroy() { sp.reset(); } weak_ptr<B> r1, r2, r3, r4; }; void testA() { vector<A *> v; for (int i = 0; i < 1000; ++i) // Create objects v.push_back(new A); for (int i = 0; i < 1000; ++i) // Initialize live references { A *a = v[i]; a->r1 = v[i % 8]; a->r2 = v[i % 32]; a->r3 = v[i % 128]; a->r4 = v[i % 512]; } for (int i = 0; i < 1000; ++i) // Juggle live references { A *a = v[i]; a->r1 = a->r2; a->r2 = a->r3; a->r3 = a->r4; a->r4 = a->r1; } for (int i = 0; i < 1000; ++i) // Delete the objects delete v[i]; } void testB() { vector<B *> v; for (int i = 0; i < 1000; ++i) // Create objects v.push_back(new B); for (int i = 0; i < 1000; ++i) // Initialize live references { B *b = v[i]; b->r1 = v[i % 8]->get_ref(); b->r2 = v[i % 32]->get_ref(); b->r3 = v[i % 128]->get_ref(); b->r4 = v[i % 512]->get_ref(); } for (int i = 0; i < 1000; ++i) // Juggle live references { B *b = v[i]; b->r1 = b->r2; b->r2 = b->r3; b->r3 = b->r4; b->r4 = b->r1; } for (int i = 0; i < 1000; ++i) // Delete the objects v[i]->destroy(); } int main() { clock_t t1 = clock(); for (int i = 0; i < 1000; ++i) test1(); clock_t t2 = clock(); cout << "T1: " << double(t2 - t1) / CLOCKS_PER_SEC << endl; t1 = clock(); for (int i = 0; i < 1000; ++i) test2(); t2 = clock(); cout << "T2: " << double(t2 - t1) / CLOCKS_PER_SEC << endl; return 0; }
The problem with providing such an interface is lack of reentrancy and thread safety. Consider this code:
That is a valid remark, I didn't think about it before. My applications never used live_ptrs across threads. I see several solutions to that problem: 1. provide locking interface (as in weak_ptr) instead of direct access 2. leave is at it is, and let user deal with synchronization, if any is needed (not every class must be thread safe) 3. provide two separate classes, one implementing 1. and the other 2.
Your live_ptr<> requires support from Tank, right?
Yes, current implementation requires a class to inherit from live_ptr_target<T>. But I understand that being 2x faster and a little more straightforward to use might not be enough to mandate addition to boost. cheers, Marcin Kalicinski

Marcin Kalicinski wrote:
[live_ptr | shared_ptr/weak_ptr] Single threaded: 0.921s | 1.891s (live_ptr is 2x faster) Multi threaded: 1.015s | 8.063s (live_ptr is 8x faster)
Below is source code for that test:
#include <iostream> #include <ctime> #include <vector> #include <boost/weak_ptr.hpp> #include <boost/shared_ptr.hpp> #include "live_ptr.hpp"
Can you post live_ptr.hpp so that I can run the test and play with it a bit?

Can you post live_ptr.hpp so that I can run the test and play with it a bit?
#ifndef LIVE_PTR_INCLUDED #define LIVE_PTR_INCLUDED #include <boost/assert.hpp> template<class T> class live_ptr; /////////////////////////////////////////////////////////////////////////// // Class live_ptr_target template<class T> class live_ptr_target { friend live_ptr<T>; public: live_ptr_target(); live_ptr_target(const live_ptr_target<T> &); ~live_ptr_target(); live_ptr_target<T> &operator =(const live_ptr_target<T> &); private: mutable live_ptr<T> *_list; }; /////////////////////////////////////////////////////////////////////////// // Class live_ptr template<class T> class live_ptr { friend live_ptr_target<T>; public: live_ptr(); live_ptr(T *); live_ptr(const live_ptr<T> &); ~live_ptr(); live_ptr<T> &operator =(T *); live_ptr<T> &operator =(const live_ptr<T> &); T *operator ->() const; T &operator *() const; T *get() const; private: T *_ptr; mutable live_ptr<T> *_prev, *_next; }; // Implementation (originally in a separate file) /////////////////////////////////////////////////////////////////////////// // Class live_ptr_target template<class T> live_ptr_target<T>::live_ptr_target(): _list(0) { } template<class T> live_ptr_target<T>::live_ptr_target(const live_ptr_target<T> &): _list(0) { } template<class T> live_ptr_target<T>::~live_ptr_target() { while (_list) { _list->_ptr = 0; _list = _list->_next; } } template<class T> live_ptr_target<T> &live_ptr_target<T>::operator =(const live_ptr_target<T> &) { return *this; } /////////////////////////////////////////////////////////////////////////// // Class live_ptr template<class T> live_ptr<T>::live_ptr(): _ptr(0) { } template<class T> live_ptr<T>::live_ptr(T *ptr): _ptr(ptr) { if (_ptr) { live_ptr_target<T> *target = _ptr; if (target->_list) target->_list->_prev = this; _prev = 0; _next = target->_list; target->_list = this; } } template<class T> live_ptr<T>::live_ptr(const live_ptr<T> &ptr): _ptr(ptr.get()) { if (_ptr) { live_ptr_target<T> *target = _ptr; if (target->_list) target->_list->_prev = this; _prev = 0; _next = target->_list; target->_list = this; } } template<class T> live_ptr<T>::~live_ptr() { if (_ptr) { if (_prev) _prev->_next = _next; else { live_ptr_target<T> *target = _ptr; target->_list = _next; } if (_next) _next->_prev = _prev; } } template<class T> live_ptr<T> &live_ptr<T>::operator =(T *ptr) { if (_ptr == ptr) return *this; if (_ptr) { if (_prev) _prev->_next = _next; else { live_ptr_target<T> *target = _ptr; target->_list = _next; } if (_next) _next->_prev = _prev; } _ptr = ptr; if (_ptr) { live_ptr_target<T> *target = _ptr; if (target->_list) target->_list->_prev = this; _prev = 0; _next = target->_list; target->_list = this; } return *this; } template<class T> live_ptr<T> &live_ptr<T>::operator =(const live_ptr<T> &ptr) { return operator =(ptr.get()); } template<class T> T *live_ptr<T>::operator ->() const { BOOST_ASSERT(_ptr); return _ptr; } template<class T> T &live_ptr<T>::operator *() const { BOOST_ASSERT(_ptr); return *_ptr; } template<class T> T *live_ptr<T>::get() const { return _ptr; } #endif

What you are doing is, you have weak_ptr without shared_ptr. You make all pointers weak, allowing anyone to delete the object at any time, which automatically invalidates all weak pointers. I'm a game programmer myself, and I've used similar solution in the past, but now I would use shared_ptr/weak_ptr instead. One advantage of the shared_ptr/weak_ptr combination is that it is thread-safe. You know how important this will be for nextgen console hardware. Another advantage is that you get to choose between weak and strong pointers; it is important to be able to express ownership. In other words, shared_ptr and weak_ptr compliment each other and allow you to pick the proper type of reference for each situation. Finally, in your solution, you *assume* that the only way to destroy an object is operator delete. With shared_ptr, the destruction procedure is abstracted and hidden from client code. This is a very valuable optimization tool: as soon as profiling indicates a problem with the allocation/deallocation strategy for some shared_ptr managed object(s), you can change it without any effect on client code. For example, an object representing a puff of smoke from a bullet hitting the wall can probably be optimized by not using new/delete. If you use shared_ptr to manage such objects, changing the allocation/deallocation strategy is trivial and will not break client code. The only way to achieve the same level of flexibility with your live_ptr design is to disallow new/delete and restrict everyone to using a pair of create/destroy functions. The problem is, this is not "built-in" and by the time you figure the allocations are slow you have so many new/delete calls that it is a major hassle to do something about it. Using shared_ptr gives you this extra peace of mind knowing you're in control even if you didn't know you need it. Also note that with your live_ptr, deleting an object needs linear time to the number of live live_ptrs. With shared_ptr/weak_ptr, the time it takes to destroy the managed object is independent of the number of live weak_ptrs (though I should say you can implement live_ptr semantics with const time delete as well.) Emil
participants (5)
-
Emil
-
Marcin Kalicinski
-
Marcin Kalici�ski
-
Pavel Vozenilek
-
Peter Dimov