0

I need some kind of class to manage the lifetime of singleton objects. I tried adding a method like this to my classes

static const FixedOutlineRect2D &instance() {
    static const FixedOutlineRect2D self{};
    return self;
}

Which has the nice property of constructing the object the first time it's called, but then it's not destroyed until the program is terminated which is messing up the order of deletions.

So I was thinking I could have some kind of "singleton factory" that would tie the lifetimes of all the objects to the factory and then I can destroy the whole thing as needed.

Here's what I've got so far:

class SingletonFactory {
  public:
    template<class T>
    const T &get() {
        if(!_map.template contains(typeid(T))) {
            _map.template emplace(typeid(T), new T);
        }
        return *static_cast<const T*>(_map.at(typeid(T)));
    }

    ~SingletonFactory() {
        for(const auto& p : _map) {
            delete p.second;
        }
    }

  private:
    std::unordered_map<std::type_index, const void*> _map{};
};

Usage would be like:

const Rect& rect = factory.get<FixedOutlineRect2D>();

Which would either coinstruct a new instance if one doesn't yet exist or return an existing instance.

But what I can't figure out is how to delete the instances. I'm getting an error:

Cannot delete expression with pointer-to-'void' type 'const void *'

Which makes sense because it can't know how many bytes to free unless it knows the type.

Can I get the type back out of the key so that I can cast and delete it? Or is there a better way to do what I'm trying?


This compiles and runs now:

class SingletonFactory {
  public:
    template<typename T, typename... Args>
    const T &get(Args &&... args) {
        // std::decay_t should be used
        auto &cache = getCache<T, std::decay_t<Args>...>();

        // Creating tuple from the arguments
        auto arguments = std::forward_as_tuple(std::forward<Args>(args)...);

        // Search for object in the cache
        auto it = cache.find(arguments);

        if (it != cache.end()) {
            // Found. Return.
            return *it->second;
        }

        // Not found. Add to cache.
        auto *object = new T(std::forward<Args>(args)...);
        cache.emplace(std::make_pair(std::move(arguments), object));
        return *object;
    }

  private:
    template<class T, class...Args>
    static std::map<std::tuple<Args...>, const T *> &getCache() {
        static std::map<std::tuple<Args...>, const T *> cache; // only run once
        return cache;
    }
};

(stolen and slightly modified from C++ templates std::tuple to void* and back)

But I still don't know how to clear out the caches... if I leave them as std::shared_ptr as OP had them it doesn't help exactly, they're still destructed at the end of the program. How can I iterate over all the static caches? If I had a map of caches/maps I could do it, but I don't think I can.

mpen
  • 272,448
  • 266
  • 850
  • 1,236
  • Consider pairing each pointer with a `std::function deleter`. Then at creation time you can store exactly what deletion should do. – Drew Dormann Nov 28 '21 at 23:34
  • 1
    How about make a singleton which contains all the object you need in that singleton? Then you don't have to take care of deletion and it could be initialized at startup. Or lazy construction is important in your use case? – Louis Go Nov 29 '21 at 01:03
  • 1
    @LouisGo Lazy construction isn't *really* important... might even be better for stable performance (it's a game), *but* it's a lot easier for me dev-wise. – mpen Nov 29 '21 at 07:08
  • Then Dependency-Injection frameworks might help as well. However I am not sure if there is a good one in C++... – Louis Go Nov 29 '21 at 07:24

1 Answers1

3

delete pointer to void will not call constructor ~T. You should store deleter of T. For example

std::unordered_map<std::type_index, std::pair<void*, void(*)(void*)>> _map;
_map.emplace(typeid(T), std::make_pair(new T, [](void* p){ delete static_cast<T*>(p); }));
for (const auto& p : _map) {
  p.second.second(p.second.first);
}

I suggest making template SingletonFactory::get() a static member, and a static SingletonFactory::clean() instead of the destructor.

class SingletonFactory {
  public:
    template<class T>
    static const T &get() {
        if(!_map.count(typeid(T))) {
            _map.emplace(typeid(T), std::make_pair(new T, [](const void* p){ delete (T*)(p); }));
        }
        return *static_cast<const T*>(_map.at(typeid(T)).first);
    }

    static void clean() {
        for(const auto& p : _map) {
            p.second.second(p.second.first);
        }
    }

  private:
    static std::unordered_map<std::type_index, std::pair<const void*, void(*)(const void*)>> _map;
};

std::unordered_map<std::type_index, std::pair<const void*, void(*)(const void*)>> SingletonFactory::_map;
273K
  • 29,503
  • 10
  • 41
  • 64
  • 1
    You certainly [can delete a const pointer](https://stackoverflow.com/questions/755196/deleting-a-pointer-to-const-t-const). `const` objects are only `const` after construction and before deletion. – Drew Dormann Nov 28 '21 at 23:39
  • OK now it compiles after your latest edit, but is there no way to do it with a destructor? Would sure be nice if it auto-cleaned itself. – mpen Nov 28 '21 at 23:56
  • Destructors are not nice here. What if a user of your SingletonFactory creates another SingletonFactory object and gets two instances of same T? – 273K Nov 28 '21 at 23:58
  • @S.M. Perfectly fine. Would be a waste of memory and performance but wouldn't actually cause problems. Everything I want to put in there is immutable, but they're holding onto some GPU resources so I'd like to conserve memory. – mpen Nov 28 '21 at 23:59
  • @mpen I showed you idea and the correct way of deletion, I hope. Other decisions, static or not static members are up to you. – 273K Nov 29 '21 at 00:02
  • Alright, thanks. I'll fiddle with it. I'm getting linker errors as soon as I tried integrating it. – mpen Nov 29 '21 at 00:03