2

There is a static shared_ptr<MyClass> get() that has a weak_ptr inside that gives away shared pointers using ptr.lock().

When ptr.lock() gives an empty pointer, the singleton should be created again.

But does it guarantee (it doesn't) that the destructor of the previous singleton has completed? What can be done about that?

1
  • You should have some mechanism of synchronisation in place to access/create/destroy your singleton... (wonder who own it btw, and if it is release as soon as no longer use, seems that a local variable do the job). Commented Dec 18, 2015 at 18:28

2 Answers 2

1

But does it guarantee (it doesn't) that the destructor of the previous singleton has completed? What can be done about that?

It's an unusual request, but I can see how it might be necessary if you're controlling an external singleton resource.

here's my solution.

make sure you check it thoroughly before using it in production

#include <memory>
#include <mutex>
#include <condition_variable>

struct tricky_object
{

};


class tricky_cache
{
    struct statics {
        std::mutex _m;
        std::condition_variable _deleted;
        bool _exists = false;
        std::weak_ptr<tricky_object> _cache;
    };

    static statics& get() {
        static statics _;
        return _;
    }

public:
    static
    std::shared_ptr<tricky_object> acquire()
    {
        // get static data
        auto& data = get();

        // lock the cache's mutex
        auto lock = std::unique_lock<std::mutex>(data._m);
        std::shared_ptr<tricky_object> candidate;

        // wait on the condition variable for the following conditions to be true:
        data._deleted.wait(lock, [&data, &candidate] {

            // either the object is in play and we have acquired another reference...
            candidate = data._cache.lock();
            if (candidate)
                return true;

            // ... or (if not) the previous object is actually dead and buried.
            return !data._exists;
        });

        // at this point we still own the lock and wait must have returned true, so...
        // if we own the candidate then it was already in play
        if (candidate)
            return candidate;

        // otherwise the previous object is certainly destroyed and we may create another
        data._cache = candidate = std::shared_ptr<tricky_object>(new tricky_object(),
                                                            [&data](tricky_object*p) {
                                                                // but the custom deleter needs some trickery
                                                                delete p;
                                                                if (p) {
                                                                    auto lock = std::unique_lock<std::mutex>(data._m);
                                                                    data._exists = false;
                                                                    lock.unlock();
                                                                    data._deleted.notify_all();
                                                                }
                                                            });
        // and we should record the fact that the object now exists...
        data._exists = true;
        lock.unlock();
        // ... and inform all waiters that they may continue acquiring
        data._deleted.notify_all();
        return candidate;
    }
};


int main()
{
    auto p = tricky_cache::acquire();

    return 0;
}
Sign up to request clarification or add additional context in comments.

2 Comments

Nice, I was trying to put the deleter stuff into a destructor of a wrapper and also wasn't able to do this with less than 2 mutexes. Other thought: if the acquire() is used frequently, lock() can be called before locking the mutex (but it'll hurt the readability of the hard things).
@Velkan you must acquire the mutex lock before attempting to lock() the cache, otherwise you'll have a race between failing to lock and acquiring the cache mutex. I did think about optimistically locking the _cache before bothering with the condition variable, but it starts to repeat logic and in any case, the optimiser can take care of it.
0

What can be done about that? Stop using terrible programming styles.

If you're going to use a singleton, then it should be a singleton: one instance, period. There's no need for managing its lifetime with smart pointers; it's always there. What's the point of destroying it only to recreate it later? Especially if the recreation function isn't given special parameters to recreate it later?

However, to your question:

does it guarantee (it doesn't) that the destructor of the previous singleton has completed

Does it matter? In order for the object's destructor to be started, the count of shared_ptr references to the object must be zero. So the weak_ptr is already empty. An object's lifetime ends when its destructor starts (just as an object's lifetime begins when its constructor completes). So the singleton itself is already destroyed; you're just doing cleanup work.

So there is no problem with creating a new instance of the singleton within the callstack of the old instance's destructor. It simply won't be accessing itself.

Threading

In a multithreaded environment, this kind of interface is already terribly broken without some kind of lock within the get function that returns/creates the singleton. Without such mutual exclusion, multiple threads could try to create the singleton simultaneously, which could cause the construction of multiple singleton instances.

As for resources within the singleton itself, the release of such resources has to be governed by some form of mutual exclusion mechanism already. The only time when a resource is itself a singleton. But unlike the singleton we're talking about, it would be one that cannot be owned by multiple pieces of code.

In that case, your singleton shouldn't have ownership of that resource at all. It can reference it, but it cannot destroy or create it.

2 Comments

"Stop doing what you're doing!" is the motto of the stackoverflow. I don't want to care about what happens in the destructor (it's not even my class), I just assume that it's nasty and that I want a clean one-function get() interface to that. (and the guys next door are using that code through some incomprehensible hyper-flexible python-thing that is used to unloading of the random parts of the program at unexpected times)
The object's lifetime ends when the destructor starts but the resources managed by it have lifetimes that are a little longer. This is not a problem in a single-threaded environment but can cause races in a multi-threaded one - particularly when managing en external resource (database connections, sockets, file handles, shared memory locks and so on). The question is unusual but valid and important.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.