13

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;
}

Question

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.

Minor precision

I do not intend to use this in multithreaded code. If the need ever arises, I'll be sure to reconsider the whole thing.

Quentin
  • 62,093
  • 7
  • 131
  • 191
  • Looks fine to me. You might want to add another test by assigning `obj.lock` to a `shared_ptr` and make sure it hasn't expired at that point, but I don't expect it to make any difference. – Mark Ransom Jan 09 '15 at 18:00
  • Doesn't requiring the caller doing an explicit "destroy" step defeat the purpose of using shared_ptr in the first place? – Billy ONeal Jan 09 '15 at 18:08
  • I think the `die()` method refers to a game character actually "dieing", like a spaceship getting blown to smithereens or something like that. e.g. `if (spaceship->hitpoints() <= 0) spaceship->die()` – Jason Jan 09 '15 at 18:12
  • @BillyONeal it gives you the ability to have `weak_ptr` copies that you can test to see if the object still lives or not. – Mark Ransom Jan 09 '15 at 18:13
  • @BillyONeal not really, because I'm using it for an unintended purpose. I'm going to change the title too. – Quentin Jan 09 '15 at 18:13
  • In terms of behaviour, it seems OK (but, obviously, the "die" function is not guaranteed to cause any actual deletion, it just removes one reference out of many ("locked" pointers)). It seems very wasteful to use shared-ptr for this purpose, this type of pattern ("suicide objects") are typically implemented with an [intrusive reference count](http://www.drdobbs.com/cpp/a-base-class-for-intrusively-reference-c/229218807). Finally, I would discourage the use of the "suicide objects" pattern, it's an ancient and dangerous practice from 90's era C++ programming practice. – Mikael Persson Jan 09 '15 at 18:13
  • Chromium codebase has something like `WeakPointerFactory` which is exactly what you are trying to implement. They use their own `shared_ptr` and `weak_ptr` so you cannot directly use that code. It might inspire you however about how to deal with it design-wise. – Mateusz Kubuszok Jan 09 '15 at 18:15
  • @MikaelPersson The object *should* die almost immediately, because there shouldn't be any forgotten locked pointer (or else it's a bug). `shared_ptr` is indeed overkill since I use only half of its functionality, but I'm not sure I want to implement it myself for now (and definitely not for a POc). Thanks for the article, it will surely be useful ! – Quentin Jan 09 '15 at 18:19
  • @maddening going to have a look, thank you ! – Quentin Jan 09 '15 at 18:25
  • @Quentin "there shouldn't be any forgotten locked pointer" That's an assumption that you cannot enforce with this code. That's part of the problem with this design. And the other part of the problem is that what you really want is single ownership (the entity that creates the object and eventually calls "die()" on it, is the single owner the object) but you are implementing it through a mechanism for shared ownership (shared-ptr or some intrusive ref-count). If you need single / unique ownership, you don't need this solution, and if you need shared ownership, this solution is not appropriate. – Mikael Persson Jan 09 '15 at 18:27
  • @MikaelPersson I believe I'm a third case, as anyone can destruct the object. So it's not unique ownership, but not shared ownership either as no one can hold it alive for oneself. For the stray `shared_ptr`s, what about wrapping the locked `shared_ptr` in a non-copyable, non-movable, non-anything object ? – Quentin Jan 09 '15 at 18:35

3 Answers3

3

When you have shared_ptr based object lifetime, the lifetime of your object is the "lifetime" of the union of the shared_ptrs 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

Community
  • 1
  • 1
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • I think the pImpl solution is an excellent suggestion. It addresses many of the issues I brought up in my answer and gives you a lot of flexibility. That's got my vote. – Jason Jan 09 '15 at 18:28
3

Not a direct answer but potentially useful information.

In Chromium codebase there is a concept of exactly what you are trying to achieve. They call it WeakPtrFactory. Their solution cannot be directly taken into your code since they have their own implementation of e.g. shared_ptr and weak_ptr but design wise it can be of use to you.

I made a try to implement it and found out that the problem of double deletion can be solved by passing into inner shared_ptr custom empty deleter - from this moment on neither shared_ptrs created from weak_ptr not inner shared_ptr will be able to call destructor (again) on your object.

The only problem to solve is what if your object is deleted and somewhere else you keep shared_ptr to it? But from what I see it cannot be simply solved by any magic mean and require designing that whole project the way that it simply never happens e.g. by using shared_ptr only in local scope and ensuring that some set of operations (creating suicide object, using it, ordering its suicide) could be performed only in the same thread.

Mateusz Kubuszok
  • 24,995
  • 4
  • 42
  • 64
2

I understand you're trying to create a minimal example for SO, but I see a few challenges you'll want to consider:

  1. You have a public constructor and destructor, so technically there's no guarantee that the create() method is always used.
  2. You could make those protected or private but that decision would interfere with use with std algorithms and containers.
  3. This doesn't guarantee that the object will actually destruct because as long as someone has a shared_ptr it's going to exist. That may or may not be a problem for your use case, but because of that I don't think this will add as much value as you're heading.
  4. This is likely going to be confusing and counter-intuitive to other developers. It might make maintenance harder, even if your intent is to make it easier. That's a bit of a value judgement, but I'd encourage you to consider if it's truly easier to manage.

I commend you for putting thought into memory management up front. Disciplined use of shared_ptr and weak_ptr will help with your memory management issues -- I'd counsel against trying to have the instance try to manage its own lifecycle.

As for art studies... I'd only recommend that if that's truly your passion! Good luck!

Jason
  • 1,086
  • 5
  • 10
  • Your first point is just because I'm a dummy, edited OP ;) — I think I'm conveying "don't hold onto that" through the exclusive exposure of `weak_ptr`, if that's true there should be only one exterior `shared_ptr` at once. Edit : wait, it doesn't compile as-is with a private constructor. Really good point. – Quentin Jan 09 '15 at 18:23
  • Yep, but now you've disqualified yourself from using much of the std library. I like Yakk's suggestion on a pImpl approach. – Jason Jan 09 '15 at 18:29