The lifetimes of various objects referring to each other are sometimes only known at runtime. For example, in a side scrolling shooter game, a HomingMissile
instance might have a pointer to the Target
instance it is following. But if the Target
is killed before the missile arrives, the HomingMissile
is left with a dangling pointer to where the Target
was in memory, likely causing a segmentation fault.
It would be nice if pointers could just be checked for validity, but it is obvious that the following example for detecting wether or not ptrToFoo
is valid by comparison with nullptr
will fail, as a pointer is merely an address, having no knowledge about the lifetime of the pointed object.
struct Foo
{
const char *hello() const
{
return "hello world";
}
};
int main()
{
Foo *foo = new Foo;
Foo *ptrToFoo = foo;
std::cout << ((ptrToFoo == nullptr) ? "expired!" : ptrToFoo->hello()) << "\n";
delete foo;
std::cout << ((ptrToFoo == nullptr) ? "expired!" : ptrToFoo->hello()) << "\n";
return 0;
}
//output:
//hello world
//hello world <- bad
C++11 smart pointers are great, but they do not feel like the right tool for the job. It makes no sense for the HomingMissile
to own the Target
trough a std::shared_ptr
or a std::unique_ptr
. Having HomingMissile
hold a std::weak_ptr
to the Target
does allow checking the state of the target
instance, but it forces the Target
to be owned trough a std::shared_ptr
by some unrelated third party. In addition, constantly calling weak_ptr::lock()
is cumbersome at least.
As a solution, I have been writing a different kind of smart pointer, one that does not imply any kind of ownership at all. The idea is to have a reference-count-based pointer-like class that knows if the pointed object has been destroyed or not, preventing the "dangling pointer" problem:
struct Foo : public ObserverPtr<Foo>::Observable
{
const char *hello() const
{
return "hello world";
}
};
int main()
{
Foo *foo = new Foo;
ObserverPtr<Foo> ptrToFoo(*foo);
std::cout << (ptrToFoo.expired() ? "expired!" : ptrToFoo->hello()) << "\n";
delete foo;
std::cout << (ptrToFoo.expired() ? "expired!" : ptrToFoo->hello()) << "\n";
return 0;
}
//output:
//hello world
//expired!
The ObserverPtr implementation should work for all classes derived from ObserverPtr</*derived class name*/>::Observable
, independently of allocation type (stack, heap, gobal) or any type qualifiers (const, volatile). Multithreading support is not required at this stage.
Implementation of ObserverPtr:
template<class T>
class ObserverPtr
{
public:
class Observable
{
protected:
///////////////////////////////////////////////////////////////////////
// Reset all ObserverPtrs that point to this ObserverPtr::Observable-
// ..derived object.
void resetObserverPtrs()
{
if(tracker_->referenceCount > 1)
{
tracker_->pointer = nullptr;
tracker_->referenceCount--;
tracker_ = new Tracker {static_cast<T*>(this), 1};
}
}
//The constructor and destructor are protected. This ensures that the
//..class ObserverPtr::Observable cannot exist on it's own but must be
//..inherited.
Observable()
: tracker_(new Tracker {static_cast<T*>(this), 1})
{ }
~Observable()
{
if(tracker_->referenceCount > 1)
{
tracker_->pointer = nullptr;
tracker_->referenceCount--;
}
else
delete tracker_;
}
private:
//The Tracker is a reference counted, heap allocated object that
//..is used internally by ObserverPtr system for tracking the state
//..of the pointed object.
struct Tracker
{
T *pointer;
unsigned int referenceCount;
};
Tracker* tracker_;
friend ObserverPtr;
};
///////////////////////////////////////////////////////////////////////////
// Construct a blank ObserverPtr that points to or observes nothing
ObserverPtr()
: tracker_(nullptr)
{ }
///////////////////////////////////////////////////////////////////////////
// Construct an ObserverPtr to observe an observable class.
ObserverPtr(T &observed)
: tracker_(observed.tracker_)
{
tracker_->referenceCount++;
}
///////////////////////////////////////////////////////////////////////////
// Construct an ObserverPtr as a copy of another ObserverPtr
ObserverPtr(const ObserverPtr &other)
: tracker_(other.tracker_)
{
tracker_->referenceCount++;
}
//TODO: Move constructor
///////////////////////////////////////////////////////////////////////////
// Destructor.
~ObserverPtr()
{
reset();
}
///////////////////////////////////////////////////////////////////////////
// Resets the the ObserverPtr to observe nothing.
void reset()
{
if(tracker_ != nullptr)
{
if(tracker_->referenceCount > 1)
tracker_->referenceCount--;
else
delete tracker_;
tracker_ = nullptr;
}
}
///////////////////////////////////////////////////////////////////////////
// Reset the ObserverPtr to point to and observe a different object
void reset(const T &observed)
{
if(tracker_ != observed.tracker_) //prevent resetting to itself
{
reset();
tracker_ = observed.tracker_;
if(tracker_ != nullptr)
{
tracker_->referenceCount++;
}
}
}
///////////////////////////////////////////////////////////////////////////
// Reset the ObserverPtr to observe nothing.
ObserverPtr &operator=(std::nullptr_t)
{
reset();
return *this;
}
///////////////////////////////////////////////////////////////////////////
// Reset the ObserverPtr observe the same object as another ObserverPtr
ObserverPtr &operator=(const ObserverPtr &other)
{
if(tracker_ != other.tracker_) //prevent self-assignment
{
reset();
tracker_ = other.tracker_;
if(tracker_ != nullptr)
{
tracker_->referenceCount++;
}
}
return *this;
}
//TODO: Move assignment operator
///////////////////////////////////////////////////////////////////////////
// Swap the observed objects between two ObserverPtrs
void swap(ObserverPtr &other)
{
typename ObserverPtr::Observable::Tracker *previousSentry = tracker_;
tracker_ = other.tracker_;
other.tracker_ = previousSentry;
}
///////////////////////////////////////////////////////////////////////////
// Dereference member of the object pointed by ObserverPtr.
// ..Calling this on a expired ObserverPtr results in undefined behaviour!
T *operator->() const
{
return tracker_->pointer;
}
///////////////////////////////////////////////////////////////////////////
// Dereference the ObserverPtr. Returns a reference to the pointed object.
// ..Calling this on a expired ObserverPtr results in undefined behaviour!
T &operator*() const
{
return *(tracker_->pointer);
}
///////////////////////////////////////////////////////////////////////////
// Dereference the ObserverPtr. Returns a reference to the pointed object.
// ..Calling this on a expired ObserverPtr results in undefined behaviour!
T *get() const
{
return tracker_->pointer;
}
///////////////////////////////////////////////////////////////////////////
// Return true if the ObserverPtr is expired. Returns true if the observed
// ..object has been destroyed, or if the ObserverPtr has been constructed
// ..or reset to observe nothing.
bool expired() const
{
if(tracker_ == nullptr)
return true;
if(tracker_->pointer == nullptr)
return true;
return false;
}
///////////////////////////////////////////////////////////////////////////
// Return true if the ObserverPtr is expired. Equivalent to expired().
bool operator==(std::nullptr_t) const
{
return expired();
}
///////////////////////////////////////////////////////////////////////////
// Return true if the ObserverPtr is not expired. Equivalent to !expired().
bool operator!=(std::nullptr_t) const
{
return !expired();
}
///////////////////////////////////////////////////////////////////////////
// Test two ObserverPtrs for equality. Returns true if both pointers point
// ..to the same object or if both pointers are expired.
bool operator==(const ObserverPtr &other) const
{
if(expired())
return other.expired();
else
{
if(other.expired())
return false;
else
return tracker_->pointer == other.tracker_->pointer;
}
}
///////////////////////////////////////////////////////////////////////////
// Test two ObserverPtrs for inequality. Returns false if both pointers
// ..point to the same object or if both pointers are expired.
bool operator!=(const ObserverPtr &other) const
{
return !((*this) == other);
}
private:
typename Observable::Tracker *tracker_;
};
The implementation is still work in progress:
A move constructor has not yet been implemented.
A move assignment operator has not yet been implemented.
Implicit type conversions between ObserverPtrs of different types are not supported, and I think that it should stay that way. Possibly an overload of
std::static_pointer_cast
should be provided.An ObserverPtr to a const- declared object (
ObserverPtr<const Foo>
) does not currently compile.The code has not yet been troughoutly tested.
The code is at this stage completely unoptimized.
I am still relatively new to C++ and especially C++11, so the code may not be as good as it could be, and writing a robust smart pointer is surprisingly difficult.
I would like to receive feedback on the concept in general and my implementation. I currently cannot figure out how to add support for pointing to const
objects, but I am working on it.