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

Started by
4 comments, last by Emmanuel Deloget 16 years, 9 months ago
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
Advertisement
You claim singletons are bad. Yet your entire design is singleton based.
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
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.
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
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).

This topic is closed to new replies.

Advertisement