Jump to content
  • Advertisement
Sign in to follow this  
riku

[C++] Type safe resource management for unrelated types

This topic is 4075 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

After being frustrated with C++ resource management for a long time, I came up with an idea to do type safe resource management for C++. Resource management is a subject that is particularly difficult in C++. Resource management is a very common problem in game programming. Games have to deal with enormous amounts of data as well as data that cannot be copied (such as textures and other system resource handles). There are a few common ways to address this problem. The simplest one is to use a class Resource which all "resources" in the program inherit. This concept is not type safe and is very limiting to the programmer. Resources such as textures and mesh geometry have very little in common. Such classes should not have a common base. This kind of system is very common these days as many people have a Java background and may never even have heard about type safety. Another common (wrong) method for resource management is singletons. Many people use it, even major pieces of software like Ogre deploy singleton based resource management. Hell, even gamedev.net has an article (http://www.gamedev.net/reference/articles/article1439.asp) on how to write a singleton texture manager. Singletons are about as flexible as granite. They are tempting because they are easy to write. There are some solutions which use run-time type information to provide run-time type safety. That is somewhat of a compromise. It does provide you with type safety, but it's runtime. It slows the code down and allows you to write hard-to-debug errenous code. I found out this method on how to address this problem without compromising type safety or using singletons. It is possible to create multiple resource managers, each of which can hold different types of types of data. I use boost::shared_ptr for reference counting, but this concept would work fine without (but would require additional cleanup). I will most likely add some type of manually invoked garbage collection, although it's not necessary for resource leaking but rather to free unused resources when they are needed for reuse. The code, however, is very dirty at the moment. I've only written it as a proof of concept. I have not done a lot of (read: pretty much any) testing, nor have I deployed this in a large scale application. I will upload the source for review and comments. I will keep on improving this torwards a generic solution for resource management.

// WARNING: Quick and dirty proof of concept code!!!!
// All the code below is probably trivially parametrizable
// to a generic template-based solution

#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <stdexcept>
#include <boost/shared_ptr.hpp>
#include <boost/utility.hpp>


/// PRIVATE IMPLEMENTATION:
// classes Storage and StorageBase could actually be private inner classes of DataManager
// the user does not need to know about them


class DataManager;    // forgive me for my class names

class StorageBase : boost::noncopyable
{
    public:
        virtual ~StorageBase()
        {
            Registry &reg = registry();
            reg.erase(std::remove(reg.begin(), reg.end(), this), reg.end());
        }

        static void unregister(const DataManager *mgr)
        {
            // this is a rather ugly solution and may need to be replaced with a
            // more efficient one

            Registry &reg = registry();
            for(Registry::iterator i = reg.begin(); i != reg.end(); ++i)
            {
                (*i)->unregister(mgr);
            }
        }

    protected:
        StorageBase()
        {
        }

    private:
        typedef std::vector<StorageBase*> Registry;

        static Registry& registry()
        {
            static Registry reg;
            return reg;
        }
};

template <class T>
class Storage : StorageBase
{
    public:
        static boost::shared_ptr<T> getResource(const DataManager *manager, const std::string &name)
        {
            boost::shared_ptr<Storage> ptr = getStorage(manager, false);
            return ptr->get(name);
        }

        static void addResource(const DataManager *mgr, const std::string& name, const boost::shared_ptr<T> &datum)
        {
            getStorage(mgr, true)->data[name] = datum;
        }

        void unregister(const DataManager *mgr)
        {
            Registry &reg = registry();
            reg.erase(std::remove(reg.begin(), reg.end(), mgr), reg.end());
        }

        // TODO: a function that removes all unique data

    private:
        Storage() {}

        boost::shared_ptr<T> get(const std::string &name)
        {
            typename std::map<std::string, boost::shared_ptr<T> >::iterator iter = data.find(name);
            if(iter == data.end()) throw std::runtime_error("datum not found");
            return iter->second;
        }

        std::map<std::string, boost::shared_ptr<T> > data;
        
        typedef std::map<const DataManager*, boost::shared_ptr<Storage> > Registry;

        static boost::shared_ptr<Storage> &getStorage(const DataManager *mgr, bool create)
        {
            Registry &reg = registry();
            typename Registry::iterator iter = reg.find(mgr);

            if(iter == reg.end())
            {
                if(!create) throw std::runtime_error("storage not found");
                return reg[mgr] = boost::shared_ptr<Storage>(new Storage);
            }

            return iter->second;
        }


        static Registry& registry()
        {
            static Registry reg;
            return reg;
        }
        
};

// the public interface, all the user needs to know about

class DataManager : boost::noncopyable
{
    public:
        ~DataManager()
        {
            StorageBase::unregister(this);
        }

        template <typename T>
        boost::shared_ptr<T> getResource(const std::string &name)
        {
            return Storage<T>::getResource(this, name);
        }

        template <typename T>
        void addResource(const std::string &name, const boost::shared_ptr<T> &resource)
        {
            Storage<T>::addResource(this, name, resource);
        }
};

struct A : boost::noncopyable { explicit A(int x) : x(x) {} int x; };
struct B : boost::noncopyable { explicit B(int y) : y(y) {} int y; };

int main()
{
    DataManager mgr;
    mgr.addResource("a1", boost::shared_ptr<A>(new A(1)));
    mgr.addResource("a2", boost::shared_ptr<A>(new A(2)));
    mgr.addResource("b1", boost::shared_ptr<B>(new B(3)));

    boost::shared_ptr<A> a = mgr.getResource<A>("a1");
    std::cout << a->x << std::endl;
    std::cout << mgr.getResource<B>("b1")->y << std::endl;

    DataManager mgr2;
    mgr2.getResource<A>("a1"); // this will throw an exception as it's supposed to

}

I want to hear about bugs, improvement suggestions, similar pieces of software and related material (books, articles, etc) and all comments are welcome. -Riku

Share this post


Link to post
Share on other sites
Advertisement
Quote:
Original post by Antheus
You claim singletons are bad. Yet your entire design is singleton based.


I agree that my design uses rather many static members (something that should be improved), but it does _not_ use a singleton (public) inteface. What is improved in comparison to a singleton based design is that you can create multiple resource managers with different data and the data is destroyed when a resource manager is destroyed. This allows a lot more flexibility than having half a dozen (one for each type) of singleton based managers (which may be used through one interface).

-Riku

Share this post


Link to post
Share on other sites
I agree with riku. While the implementation of the design does indeed use global instances that look suspiciously like singletons, these are still merely implementation details. The interface of the system does not rely on this globality at all, and actually manages to hide it completely at the cost of a few additional lookups.

In short: it's singleton-based at the language level, but not at the design level.

Share this post


Link to post
Share on other sites
The main problem with this design is that you are trying to make your resource management aware of the types of resources held within it and provide type safety for the data within. This, I beleive, is extending the scope of the design beyond what it needs to be. Remember, Keep It Simple!

I think a resource management system should be an data abstraction interface that allows the program to access named data. This named data is then passed to a codec that transforms the data into a specific type.

Given the above, sample loading code would look like:

IDataStream *stream = resource_database->FindResource (resource_name);
Thing *thing = ThingCodec::Decode (stream);

Here, FindResource would search the database for the given data and return an object that implements an IDataStream interface - you may need several of these depending on how the data is stored - data could be stored in individual files, in zip files, in remote databases and so on.

The ThingCodec class is a transformation class that can convert objects to and from data streams. In the above, the Decode function creates a new instance of an object.

There are several advantages here. First off, new types require no changes to the resource database objects, either through template instantiation or through any form of registration. The Codec classes do not need to know about the resource database classes, they only operate on streams which can be created through other means in addition to the resource database. The objects themselves have no serialisation code (the codec would probably need to be a friend class to do this).

You may be thinking that there is a potential type safety issue in that resource data could be misinterpreted, i.e. a text resource could be interpreted as a bitmap. As shown above this is true but it wouldn't be difficult to add this functionality:

IDataStream *stream = resource_database->FindResource (resource_name, ThingCodec::Name);
Thing *thing = ThingCodec::Decode (stream);

Here, the FindResource function is effectively using two keys to search the database. Entries in the database would be of the form:

Name Type Data
--------------------------------
obj1 string some text
obj1 image image data
obj2 image another image data

which would allow names to be duplicated providing the type is different.

Skizz

Share this post


Link to post
Share on other sites
I dislike the getStorage() method - mainly because it takes a second parameter to specify if the storage should be created or not. This is not something that is really that good - I would have preferred to see two different functions - getStorage() and createAndGetStorage() - that makes things more, well, readable.

Here should go the rant about using boolean as parameters. but I don't have much time, so to keep it short: boolean function parameters are impossible to read; use an enum instead (even better is the enum is encapsulated in its own class).

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!