boost::weak_ptr and boost::unordered_map/set

Started by
9 comments, last by SiS-Shadowman 13 years, 9 months ago
I've implemented several systems that store a list of created objects, without owning those objects. One example is my D3DDevice, where I needed to recreate buffers in a lost device scenario. Using a set of shared_ptr was not really an option: it would require to call D3DDevice::release() to remove an object from the list again. Instead I wanted to use RAII semantics: Once the list shared_ptr of an object runs out of scope, the device is notified of this and removes it from the list.
Since it's impossible to store a weak_ptr in a boost::unordered_set / map (as key), I stored a raw pointer instead.

However I revisited the problem today and came up with a dirty hack that worked for me so far:
template<class T> inline bool operator==(weak_ptr<T> const &a, weak_ptr<T> const & b){	return !a._internal_less(b) && !b._internal_less(a);}template<class T> std::size_t hash_value(weak_ptr<T> const & a){	static_assert(sizeof(std::size_t) == sizeof(T*), "std::size_t must have the same size as a pointer");	static_assert(sizeof(weak_ptr<T>) == 2 * sizeof(void*), "Check this hack, the size of weak_ptr has been changed, since the initial implementation");	// Awesome... offsetof cannot acces a private member, therefore I cannot guarantuee that this will even work	const T* px = reinterpret_cast<const T*>(*reinterpret_cast<const std::size_t*>(&a));	// Well, you can see in the debugger that both a.px and px have the same value :-)	return hash_value(px);}


Unfortunately, I cannot guarantee that px is the first member of weak_ptr which I don't really like. Another problem is the required == operator. Do you know a way to hide it from everything but unordered_set without changing weak_ptr's interface?

I'm aware this is a dirty hack, but at least it's now possible to store an unordered_set<weak_ptr>, so I can avoid raw pointers completely.

*edit*
Do you have any suggestions on this code, perhaps you know of cases when it will most certainly fail.
Advertisement
I haven't really thought this through, but one way to "hide" your weak_ptr operator== from general use.

Use unordered_set's Pred template parameter, this defaults to std::equal_to<Key> - which I guess in turn calls operator==. If you provide your own predicate it can call any function you like (or even include code from your operator directly) This would allow you to avoid operator== and write code using a function name that does not expose a change to weak_ptrs.
Quote:Original post by TomH
I haven't really thought this through, but one way to "hide" your weak_ptr operator== from general use.

Use unordered_set's Pred template parameter, this defaults to std::equal_to<Key> - which I guess in turn calls operator==. If you provide your own predicate it can call any function you like (or even include code from your operator directly) This would allow you to avoid operator== and write code using a function name that does not expose a change to weak_ptrs.


Thanks for the advise. This should stop me from (mis)using operator== on a weak_ptr.

Another thing that really bugs me is that I have no way of knowing if the offset of 'px' is actually 0 bytes. There is no compiler extension (for msvc, that I know of) that is capable of exposing that information during compile time. I think I will place another static_assert that checks if the boost version has changed to the one I implemented this hack, so I constantly revisit this code when I migrate to a newer version.
You should use an unordered_map with a raw pointer as the key in my opinion. Makes the hashing function straightforward (how do you hash an expired weak_ptr?).
Quote:Original post by Shinkage
You should use an unordered_map with a raw pointer as the key in my opinion. Makes the hashing function straightforward (how do you hash an expired weak_ptr?).


The same way I hash a pointer to an instance of a class that has already been deleted: Both the raw pointer and weak_ptr preserve the address.
The only problem that might come up is that converting a weak_ptr from type Y to T requires a shared_ptr to be present. I don't really know why this is required (the documentation states this is due to virtual inheritance, however I don't know how this is implemented in the first place).
But this is no problem for my implementation so far.

My idea to use a weak_ptr in the first place is that I don't want any sporadic crashes because I accessed an object that has been deleted long before (and unfortunately we have too many of those at work): With weak_ptr either an exception is thrown, or I receive an empty shared_ptr (depending on the method I use to obtain the shared_ptr). Both ways can be dealt with, whereas a raw pointer might show undefined behaviour.

I admit that there is no need to use a weak_ptr in this case though: It's personal preference.
Can you clarify the problem please? Are you just wanting the item to be removed from the list when the pointer goes out of scope or is destroyed?

Your first post got me wondering, what are the alternatives to casting for doing operations on the raw pointer. Admittedly this is still deep in "hack country", but you can use template specialisation to provide the operations you want - in reality this is not much better than macro magic (#define private public, #include weak_ptr...)

A solution would look something like this:

#include "boost/weak_ptr.hpp"#include "boost/functional/hash.hpp"#include <iostream>namespace{  struct WeakCmpOp   {  };}namespace boost{  template<>  class weak_ptr<WeakCmpOp>  {    public:      template<typename T>      static std::size_t hash( const weak_ptr<T>& victim )      {         boost::hash<T*> hasher;        return hasher(victim.px);       }      template<typename T>      static bool isEqual( const weak_ptr<T>& victimA, const weak_ptr<T>& victimB )      { return victimA.px == victimB.px; }  };}using namespace std;int main(){  boost::weak_ptr<int> ptr;  cout << "count = " << ptr.use_count() << endl;  cout << "hash = " << boost::weak_ptr<WeakCmpOp>::hash(ptr) << endl;  cout << "test = " << boost::weak_ptr<WeakCmpOp>::isEqual(ptr, ptr) << endl;  return 0;}


On balance this sort of thing will probably "bite you" sooner or later - but it was fun figuring out how it can be done ;)
Quote:Original post by SiS-Shadowman
One example is my D3DDevice, where I needed to recreate buffers in a lost device scenario. Using a set of shared_ptr was not really an option: it would require to call D3DDevice::release() to remove an object from the list again. Instead I wanted to use RAII semantics: Once the list shared_ptr of an object runs out of scope, the device is notified of this and removes it from the list.


I'm not completely sure why this is a problem; one of the things you can do with shared_ptr is specifiy a "delete" function which is called when the value held is to be deleted.

So, you could route this to a free function which takes a pointer to D3DDevice (for example), calls 'release' and then, if needs be, calls 'delete' on the supplied pointer.
It's still in the container though, so holding a reference, which means the custom deleter won't be called. I thought the idea was to remove it from the container when it was destroyed, not destroy it when it was removed from the container, if you see what I mean.

On the whole I tend to prefer explicit operations to those applied implicitly through a kind-of trick in cases like this. It makes for less obfuscated code. But I think the purpose of holding the lists in D3DDevice should be explained. Do the objects using a particular buffer ask D3DDevice for a reference to it when they need it, i.e. shared_ptr<BufferClass> GetBufferX(), or do they share the reference itself as a member variable? In the former case you can completely destroy and re-create your list of buffers without having any side-effects outside of the D3DDevice instance. In the latter case you're asking for trouble.







Nothing with boost::shared_ptr requires you to use delete on the stored pointer (it's a very convenient default though). It's not "some-kind-of-trick" to supply a custom deleter to the shared_ptr, it's the standard solution to a problem which you appear to work around in a very awkward way right now.

The really nice thing about the shared_ptr is that you don't have to care how and when to free an object. You specify the deleter function once, at allocation time (which is the only time you are guaranteed to know how exactly to deallocate the object as well). It does not matter after that how often you copy the shared_ptr after that, when it is time to delete the object, the correct function is called.

For example, imagine a scenario where MyWeirdClass can be allocated either through new or a myLibraryAllocate function and then you have to call either delete or myLibraryFree, never the wrong one. That's a trivial problem if you use a shared_ptr but starts to require a bit work to get right (probably a bit more to get it *really* right). And that is not a completely fictional scenario either if you have to rely on some libraries.

This topic is closed to new replies.

Advertisement