6
\$\begingroup\$

I've done some some research on my own, so I hope someone can help me finish it. I'm trying to replicate the design of an extensible asset manager featured in this answer.

At the moment I have created this and have no clue on how to proceed.

class ResourceManager {
    public:
        template<typename T>
        bool addFactory( std::unique_ptr<ResourceFactory<T>> factory ) {
            std::type_index ti = std::type_index(typeid(T));

            auto searchResult = factories.find(ti);
            if(searchResult != factories.end()) {
                return false;
            } else {
                factories[ti] = std::move(factory);
                return true;
            }
        }

        template <typename T>
        ResourceHandle<T> load(const std::string &path) {
            std::type_index ti = std::type_index(typeid(T));
            auto searchResult = factories.find(ti);

            if(searchResult == factories.end()) {
                throw NoValidFactory(std::string("No registered factory of type ") +
                                     ti.name());
            }

            // TODO SOMEHOW DO LOADING
            //return searchResult->second->load(path);
        }
    private:
        std::unordered_map<std::type_index, 
                           std::unique_ptr<ResourceFactoryI>> factories;
 };

I assume that TextureHandle featured in the answer is probably a typedef for something like a custom reference counted smart pointer (e.g. ResouceHandle<Texture>, like in my engine) but how do I return something like that? I'm not (YET) good with templates but I know that it's impossible to have a virtual specifier with a template method, so I CAN'T do something like this:

class ResourceFactoryInterface {
    public:
        template <typename T>
        virtual ResourceHandle<T> load(const std::string &path) = 0;
};

template <typename T>
class ResourceFactory : public ResourceFactoryInterface {
    public:
    ResourceHandle<T> load(const std::string &path) {
        // loading
    }
};

// TODO Template specializations for object types? Inherit from ResourceFactory?

What should I do? Am I even close or am I doing a complete nonsense? How should I proceed?

If someone wants to know why I need something like that: at the moment I have one huge, difficult to maintain and extend ResourceManager class with a different method (loadTexture, loadMesh, loadSound, etc.) for each resource type. Having something that can be extended dynamically would make my hobby engine's resource management system more reusable. Moreover, I have a few places where I'd benefit from instantiating separate resource managers with only several registered (level specific) resource types.

\$\endgroup\$
  • 1
    \$\begingroup\$ I have something along these lines, which I implement with a string-keyed map of factories, gleeful casting, and crossed fingers... \$\endgroup\$ – david van brink Apr 2 '15 at 15:56
  • 1
    \$\begingroup\$ "have no clue on how to proceed" - that could be an indicative that your requirements are vague/not well defined. First figure out what you need exactly, then start writing the code. \$\endgroup\$ – glampert Apr 2 '15 at 17:29
  • \$\begingroup\$ @glampert In this case, I know what I need, but I lack the experience required to implement it on my own. \$\endgroup\$ – Manvis Apr 2 '15 at 17:53
4
\$\begingroup\$

Back in the days of C and no templates... we had void pointers. Good thing they weren't made obsolete, because you have just seen that templates aren't omnipotent gods. This situation is perfect for some void pointer hacks.

Here, I tried to make an example that was as simple as possible:

Edit: The example below makes a dangerous use of shared_ptr, please see the edit at the bottom.

    struct ResourceManager {

        typedef std::function<void* ()> TFactoryFunctor;

        std::map<std::string, TFactoryFunctor> m_factories;

        void subscribe (std::string key, TFactoryFunctor factory) {
            m_factories[key] = factory;
        }

        template <class T>
        std::shared_ptr<T> load (std::string key) {
            auto& factoryFunctor = m_factories.at(key);
            void* ptr = factoryFunctor();

            return std::shared_ptr<T>(static_cast<T*>(ptr));
        }

    };

    struct ResourceFactory {

        std::string m_path;

        void setFilePath (std::string path) {
            m_path = path;
        }

        std::string getFilePath() const {
            return m_path;
        }

        virtual void* load() =0;

        void* operator() () {
            return this->load();
        }

    };

    struct TextureFactory : ResourceFactory {

        std::shared_ptr<Texture> m_texPtr;
        Texture* m_tex;

        TextureFactory (std::string fileName) {
            setFilePath(TEXTURES_FOLDER + fileName);
        }

        void* load() override {
            if (!m_texPtr) {
                m_tex = new Texture();
                m_tex->loadFromFile(getFilePath());

                m_texPtr = std::shared_ptr<Texture>(m_tex);
            }

            return m_tex;
        }

    };

    // ---

    ResourceManager daMan;

    void subscribeTextures() {
        daMan.subscribe("hero", TextureFactory("hero.png"));
        daMan.subscribe("foe", TextureFactory("foe.png"));
        daMan.subscribe("tileset", TextureFactory("tileset.png"));
    }

    void loadHeroTexture (Hero& hero) {
        auto texPtr = daMan.load<Texture>("hero");

        hero.claimOwnership(texPtr); // Maybe you want to keep track of who's using the resource?
        hero.sprite.setTexture(*texPtr);
    }

I didn't test this code to make sure that it's functional and I'm not sure whether it's a good idea to manipulate a raw pointer that's stored in a shared_ptr in the way within TextureFactory::load, but I hope you get the idea.

In a different, more object-oriented language you would probably use object (or some other more specialized base class) and downcasts instead of void*, but C++ doesn't have such luxuries. Maybe you can make your own ResourceBase base class? You'd have to make some wrappings, do so at your own expenses.

Edit: I have found that it is dangerous to transfer a raw pointer that's already owned by a shared_ptr to another shared_ptr. The second shared_ptr will have a different reference counting from the first and it is 100% certain that the object will be destroyed at least twice.

Instead of returning void* or BaseClass*, prefer to return shared_ptr<void> or shared_ptr<BaseClass> and remember to use the aliasing constructor of shared_ptr before returning shared_ptr<T> in your equivalent of the ResourceManager::load<T> method from the example.

Something like:

    struct TextureFactory {

        std::shared_ptr<Texture> texturePtr;

        /* ... */

        std::shared_ptr<ResourceBase> load() {
            if (!texturePtr) {
                texturePtr = std::make_shared<Texture>();
                texturePtr->loadFromFile(/* ... */);
            }

            return texturePtr;
        }

    };

    /* ... */

    struct ResourceManager {

        template <class T>
        std::shared_ptr<T> load(std::string key) {
            auto& factoryFunctor = m_factories.at(key);
            std::shared_ptr<ResourceBase> ptr = factoryFunctor();

            auto castRawPtr = dynamic_cast<T*>(ptr.get());
            assert(castRawPtr);

            return std::shared_ptr<T>(ptr, castRawPtr);
        }

    };
|improve this answer|||||
\$\endgroup\$
  • \$\begingroup\$ I've based my implementation on this. Trying to use templates for everything really was a stupid decision. And like you said, I've used a Resource base class which I already had. \$\endgroup\$ – Manvis Apr 4 '15 at 9:14
  • \$\begingroup\$ @Manvis I have been studying shared_ptr's as of lately and I noticed that my example provides dangerous use of it. Please see my edited addendum, and ensure that you are using your shared pointers properly, you might end having resources being destroyed twice. :) \$\endgroup\$ – Mutoh Apr 13 '15 at 22:42
  • 1
    \$\begingroup\$ Thanks for the warning, but like I mentioned, I based my implementation on the ideas of this answer. It ended up looking quite differently and I wasn't running into this particular problem. Still, your correction will, no doubt, be useful to anyone who tries to copy the implementation in your answer. \$\endgroup\$ – Manvis Apr 14 '15 at 7:47
0
\$\begingroup\$

Some better idea based on your approach:

You have template<typename T> bool addFactory( std::unique_ptr<ResourceFactory<T>> factory ) so every factory added is a std::unique_ptr<ResourceFactory<T>>.

So your loading code should be:

template <typename T>
ResourceHandle<T> load(const std::string &path) {
  std::type_index ti = std::type_index(typeid(T));
  auto searchResult = factories.find(ti);

  if(searchResult == factories.end()) {
     throw NoValidFactory(std::string("No registered factory of type ") +
                                     ti.name());
  }

  auto loader = *static_cast<ResourceFactory<T>*>(searchResult.get());
  return loader.load(path);
}

The cast is guaranteed to work due to how the ptrs are inserted.

On how to implement the loader: Either do specialize ResourceFactory for every T or redirect it:

template <typename T>
class ResourceFactory {
    ResourceHandle<T> load(const std::string &path) {
        T* res = static_cast<T*>(doLoad(path)); // May return fancy unique_ptr
        if(!res) throw... // If required
        return {res}; // Convert to handle
    }
    protected:
    void* doLoad(const std::string &path) virtual = 0;
};

Now you can subclass ResourceFactory<T> and implement the doLoad method.

Or you can have a look at ENTT: https://github.com/skypjack/entt/tree/master/src/entt/resource There you pass the loader type into the load function.

|improve this answer|||||
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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