Reputation: 9122
We use composition, when an object has a single parent, which should care for the object's lifetime. We use unique_ptr
when in the same situation, but the object can be nullptr
.
We use shared_ptr
when several external entities may need our object, so its lifetime is extended until the last of those external entities loses interest.
Here I want to ask about another lifetime situation. What if the object needs to live the shortest of several durations?
Here is an example. Let's have a one-shot timer, which stores a functor and executes it after a the counting is complete. It makes sense to me*, that this timer object is destroyed after:
1. fulfilling its task - therefore it should be able to destroy istelf
or
2. the parent loosing interest in the timer - so the parent should be able to
destroy the object as well
Currently, I using an awkward implementation with unique pointers. What would be a good pattern / guideline / implementation of this problem?
* reasons: 1) the functor could be owning some other resources 2) the timer could have been set to a very large number, and then abandoned 3) if the parent has been destroyed, we generally don't want to invoke its callbacks
Upvotes: 3
Views: 56
Reputation: 275976
There are serious concurrency and reentrancy problems here.
When two or more bits of code have the right to delete a pointer, neither piece of code can reliably dereference that pointer, as while doing so the other could destroy it.
Similarly, any branch where you check that you have ownership can become stale the moment any other non-local (say a function call) runs, even without concurrency.
We can work around these.
template<class T>
struct shared_destroyable {
std::shared_ptr<T> lock() const {
return atomic_load<T>(ptr.get());
}
explicit operator bool() const { return (bool)lock; }
void reset( std::shared_ptr<T> pin = {} ) {
atomic_store(ptr.get(), std::move(pin));
}
shared_destroyable(std::shared_ptr<T> pin):
ptr(std::make_shared<std::shared_ptr<T>>(std::move(pin))
{}
shared_destroyable()=default;
shared_destroyable(shared_destroyable&&)=default;
shared_destroyable(shared_destroyable const&)=default;
shared_destroyable& operator=(shared_destroyable&&)=default;
shared_destroyable& operator=(shared_destroyable const&)=default;
private:
std::shared_ptr<std::shared_ptr<T>> ptr;
};
This behaves vague like a weak_ptr
shared_ptr
hybrid.
If the last one goes away, the object is destroyed.
However, if any of them .reset()
, the object is destroyed as soon as the last other code that has lock()
ed has ended its scope.
Assignment changes which is referred to.
So at use you do this:
if(auto sp = sd.lock()) {
// use sp
}
and the sp
lifetime is guaranteed to last the scope of the {}
, even if someone does a sd.reset()
within the block, in another thread, or when you call some other method.
Upvotes: 2
Reputation: 45819
I think the best solution is to have a wrapper object with a unique pointer to the actual timer object, and a getter for the timer object that returns null if it has been destroyed.
That way if the timer expires and is destroyed, the caller who hasn't lost interest isn't left holding an apparently-valid-but-actually-invalid pointer to the timer object.
Upvotes: 0