2
\$\begingroup\$

I'm currently working on a project where i need to access objects from various places, pretty much like std::shared_ptr. At a certain point (e.g. when a library is unloaded) i need to destruct these objects. It is crucial to the project that the object is really destructed at this point, i can not just hand out weak_ptr and rely that no one keeps a shared_ptr. If i would just hand out weak_ptr to other parts of the program, anyone could call lock() and store the shared_ptr, delaying or preventing destruction of the objects. This would crash my program.

What happens in the background is this: I have a small number of classes providing generic access to yet unknown types, e.g. a class Object providing a method virtual getChilds()=0;. Libraries might then inherit from those classes with a class template <O> class TemplateObject; implementing the pure virtual functions. This way other parts of the program, including a GUI, can access in a limited way objects of a type, which was unknown at compile time. But if i destruct the Object with its virtual destructor, i call code that lives in the library - which obviously fails if i have already unloaded the library. I therefore need to make sure that the Object is destructed when the library is unloaded. The GUI e.g. is then able to notice that the object is already gone and can no longer be used.

My Idea was to create two smart pointers, namely master_ptrand slave_ptr. The master_ptr behaves pretty much like a unique_ptr or like "the one and only" shared_ptr, destructing the managed object. The slave_ptr acts much like a weak_ptr, just allowing a more direct access and hiding the option to get back the shared_ptr. The managed objects would be non-copyable and non-movable to make sure there are no copies left somewhere.

My implementation looks like this:

class ObjectInvalid{};

template <typename O>
class slave_ptr {
private:
    std::weak_ptr<O> m_ptr;

public:
    slave_ptr() {}
    slave_ptr(std::shared_ptr<O> ptr) : m_ptr(ptr) {}

    O& operator*() const {
        if (m_ptr.expired())
            throw ObjectInvalid();

        return m_ptr.lock().operator*();
    }

    O* operator->() const {
        if (m_ptr.expired())
            throw ObjectInvalid();

        return m_ptr.lock().operator->();
    }

    void reset() {
        m_ptr.reset();
    }
};


template <typename O>
class master_ptr {
private:
    std::shared_ptr<O> m_ptr;
public:
    master_ptr() = default;
    master_ptr(O* ptr) : m_ptr(ptr) {}

    //non-copyable
    master_ptr(const master_ptr&) = delete;
    master_ptr& operator=(const master_ptr&) = delete;

    //movable
    master_ptr(master_ptr&& other) = default;
    master_ptr& operator=(master_ptr&& other) = default;

    O& operator*() const {
        if (!m_ptr)
            throw ObjectInvalid();

        return m_ptr.operator*();
    }

    O* operator->() const {
        if (!m_ptr)
            throw ObjectInvalid();

        return m_ptr.operator->();
    }

    //allows reset before going out of scope
    void reset() {
        m_ptr.reset();
    }

    //cast to slave
    operator slave_ptr<O>() {
        return slave_ptr<O>(m_ptr);
    }
};

A small test tells me it works as i would expect, but before implementing it in a bigger project i wanted to aks here if theres any flaws i didn't see right now or if you could hint me to some improvements.

About priorities: Easy use and safety (no possible memory leaks, objects destructed when needed, exceptions thrown afterwards) are top priorities here. Performance is a "nice to have".

\$\endgroup\$
5
  • \$\begingroup\$ Why can't you use std::weak_ptr actually? Can you please elaborate more in your question? I think it's exactly designed for that kind of situation. \$\endgroup\$ Commented May 12, 2017 at 16:20
  • \$\begingroup\$ Well, you should hope nobody pulls the rug out under you at a inopportune moment. \$\endgroup\$ Commented May 12, 2017 at 16:37
  • \$\begingroup\$ @πάνταῥεῖ I have added a bit of background information, i hope that helps to see the bigger picture. \$\endgroup\$
    – Anedar
    Commented May 12, 2017 at 16:53
  • \$\begingroup\$ @Anedar Well, as for your explanations: Why not stick to the good ole MVC pattern? \$\endgroup\$ Commented May 12, 2017 at 17:10
  • \$\begingroup\$ @πάνταῥεῖ Thats kinda what i'm aiming for. Maybe i will be able to solve the problem alltogether by better distinguishing between view and controller... \$\endgroup\$
    – Anedar
    Commented May 12, 2017 at 18:26

4 Answers 4

5
\$\begingroup\$

This is decidedly shady:

O& operator*() const {
    if (m_ptr.expired())
        throw ObjectInvalid();

    return m_ptr.lock().operator*();
}

O* operator->() const {
    if (m_ptr.expired())
        throw ObjectInvalid();

    return m_ptr.lock().operator->();
}

In the line:

return m_ptr.lock().operator->();

You create a shared pointer. Get the pointer value from the shared pointer. Destroy the shared pointer then return the pointer that was in the shared pointer.

So now you have a pointer but no guarantee that the object it points at exists anymore (as you destroyed your copy of the shared pointer).

There are a couple of operations on the master pointer that you don't support.

  • You can't test if it is empty.
    So you are playing Russian roulette every time you use it.
  • You can't use it as part of a conditional (ie no conversion to bool).
  • You don't have a constructor that takes a nullptr => std::nullptr_t
  • You have a couple of problems with implicit construction.
\$\endgroup\$
2
\$\begingroup\$

Your solution is not thread safe. Some code uses your pointer to obtain a pointer/reference to an object, but there's no guarantee that a different thread doesn't simply delete the object before the first thread manages to use it.

As I understand your question, you don't want to allow specific code to keep a reference to your object. Then simply don't offer it a smart pointer, ask for a callback and pass a direct reference to the object into it. This indicates to the caller that the object is only available within that call (one doesn't normally take a pointer to a reference for safe keeping).

\$\endgroup\$
1
  • \$\begingroup\$ Well, i kinda want other code to have a reference to that object - i just want the other code to be able to notice once the object is gone. Thanks for pointing out the thread safety though, its currently not an issue but i should definitely add it. \$\endgroup\$
    – Anedar
    Commented May 12, 2017 at 16:54
0
\$\begingroup\$

Perhaps the only way to

  • give client code a reference/pointer to an object
  • let you unload code
  • be thread safe

is to give client code a proxy for the object, that does (thread-safely) all the checking on each access (function call on the object).

\$\endgroup\$
0
\$\begingroup\$

I try to work around problems like this by restructuring the program. I'll give examples that work with multiple threads since those examples will also work single threaded.

  1. One thread (main thread) creates and maintains a linked list of items. Each item may or may not have a separate thread (item thread) that works on that object. When the item thread is done, it doesn't delete its associated item object. Instead, it sets a DeleteMe=true on that item. The main thread occasionally looks for items to delete on its linked list.

  2. One thread is adding items to a circular linked list. Another thread is processing items on that linked list. When an item gets processed, it's not deleted from the linked list. Instead, it's marked with UnsedItem=true and moves on to the next item. The thread that adds items to the circular linked list adds new nodes if the list isn't long enough. The list starts out circular with 3 unused nodes.

But I guess the simplest way to answer your question would be to use reference counting. You can have a Deleted bit on the object when you want to delete it. Other users of the object will remove their reference to it when they notice the Deleted bit. The object will be deleted when the ReferenceCount == 0. Only then, do you unload the dll.

You could create a linked list of pointers that are pointing to your object. Every time a new pointer points to the object, that pointer's address is added to your linked list. When the main object gets deleted, walk through the linked list setting all the pointers to that object to NULL. This is getting into some kind of primitive garbage collection though.

\$\endgroup\$

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.