I'm considering using "suicide objects" to model entities in a game, that is, objects able to delete themselves. Now, the usual C++03 implementation (plain old delete this
) does nothing for other objects potentially refering to the suicide object, which is why I'm using std::shared_ptr
and std::weak_ptr
.
Now for the code dump :
#include <memory>
#include <iostream>
#include <cassert>
struct SuObj {
SuObj() { std::cout << __func__ << '\n'; }
~SuObj() { std::cout << __func__ << '\n'; }
void die() {
ptr.reset();
}
static std::weak_ptr<SuObj> create() {
std::shared_ptr<SuObj> obj = std::make_shared<SuObj>();
return (obj->ptr = std::move(obj));
}
private:
std::shared_ptr<SuObj> ptr;
};
int main() {
std::weak_ptr<SuObj> obj = SuObj::create();
assert(!obj.expired());
std::cout << "Still alive\n";
obj.lock()->die();
assert(obj.expired());
std::cout << "Deleted\n";
return 0;
}
This code appears to work fine. However, I'd like to have someone else's eye to gauge it. Does this code make sense ? Did I blindly sail into undefined lands ? Should I drop my keyboard and begin art studies right now ?
I hope this question is sufficiently narrowed down for SO. Seemed a bit tiny and low-level for CR.
I do not intend to use this in multithreaded code. If the need ever arises, I'll be sure to reconsider the whole thing.
When you have shared_ptr
based object lifetime, the lifetime of your object is the "lifetime" of the union of the shared_ptr
s who own it collectively.
In your case, you have an internal shared_ptr
, and your object will not die until that internal shared_ptr
expires.
However, this does not mean you can commit suicide. If you remove that last reference, your object continues to exist if anyone has .lock()
'd the weak_ptr
and stored the result. As this is the only way you can access the object externally, it may happen1.
In short, die()
can fail to kill the object. It might better be called remove_life_support()
, as something else could keep the object alive after said life support is removed.
Other than that, your design works.
1
You could say "well, then callers should just not keep the shared_ptr
around" -- but that doesn't work, as the check that the object is valid is only valid as long as the shared_ptr
persists. Plus, by exposing the way to create shared_ptr
, you have no type guarantees that the client code won't store them (accidentally or on purpose).
A transaction based model (where you pass a lambda in, and it operates on it internally) could help with this if you want seriously paranoid robustness.
Or you can live with the object sometimes living too long.
Consider hiding these messy details behind a Regular Type (or almost-regular) that has a pImpl
to the nasty memory management problem. That pImpl
could be a weak_ptr
with the above semantics.
Then users of your code need only interact with the Regular (or pseudoRegular) wrapper.
If you don't want cloning to be easy, disable copy construction/assignment and only expose move.
Now your nasty memory management is hiding behind a fascade, and if you decide you did it all wrong the external pseudoRegular interface can have different guts.
Regular type in C++11
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With