6
\$\begingroup\$

The original question where the general concept of this implementation was discussed: Using ID's with a "scope" -like hierarchy

I have designed a resource manager as part of a game programming project that I have been working on as a hobby. Implementing as much functionality as possible in the least amount of time and effort is not my goal, instead my focus has been on learning how to design and implement a relatively large software project by myself.

An usage example for the submitted code:

//We will first create three resource contexts: the global context, and
//..two local contexts specific to each window
auto globalCtx = ResourceContext::makeGlobalContext();
auto gameCtx = globalCtx.makeLocalContext();
auto editorCtx = globalCtx.makeLocalContext();

//The virtual filesystem maps file ID:s to system paths. Since there is no
//..reason to create two separate virtual filesystems (one for each window)
//..we will make a new VirtualFs object in the global context.
auto vFilesys = globalCtx->make<VirtualFs>("/data/index.xml");

//The game's main window is created in the game's context (gameCtx).
auto mainWindow = gameCtx->make<Window>("Game", 800, 600, SDL_WINDOW_SHOWN,
                  SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);

//The editor window is created in the editor context (editorCtx). If both
//..windows were simply created in the global context, the second call to
//..globalContext->make<Window>(...) would simply return a shared_ptr to
//..the previously created game window.
auto editorWindow = editorCtx->make<Window>("Editor", 300, 600,
                    SDL_WINDOW_SHOWN, SDL_RENDERER_ACCELERATED);

//The hero belongs to the main window. Instead of passing a reference to
//..the window, we pass the game context. The Sprite code first searches
//..for a Window instance in the gameContext, and finds mainWindow, then
//..searches for a VirtualFs, which it does not find at first, but after
//..automatically searching in the parent context (globalCtx) too a
//..VirtualFs instance vFilesys is found.
Sprite hero(gameCtx, "textures/hero.png");

//The same applies to the GuiButton, but code in GuiButton gets a pointer
//..to editorWindow instead.
GuiButton startStop(editorCtx, "Start simulation",  10, 10, 280, 50); 

In short, the idea is that instead of having a single, massive, flat hierarchy containing all resources being used in the game (textures, sounds, ...), the resources are organized in a tree-shaped hierarchy similar in concept to the scope of programming languages. This also makes it quite attractive to use the same system for concepts normally not viewed as "resources" such as a window handle. For a more troughout explanation of the idea see the original question.

#include <map>
#include <memory>
#include <vector>

class ResourceContext;

///////////////////////////////////////////////////////////////////////////////
// class Resource
//================
//class Resource is a common base class that has to be inherited by all classes
//..that are to be managed using the "ResourceContext" system. All classes
//..inheriting Resource will inherit member ResourceContext &context_ that can
//..be used to access the ResourceContext object managing said class.
//Usage:
//    Class DerivedResource : public Resource
//    {
//        DerivedResource(ResourceContext &context, ...) : Resource(context)
//        {
//            ...
///////////////////////////////////////////////////////////////////////////////
class Resource
{
public:
    Resource(ResourceContext &context) : context_(context){}
    //Virtual dtor required as shared_ptr will delete trough a base class ptr
    virtual ~Resource(){}
protected:
    ResourceContext &context_;
};

///////////////////////////////////////////////////////////////////////////////
// class ResourceContext
//=======================
//All classes inheriting "Resource" are to be managed by class ResourceContext.
//ResourceContexts form a treelike hierarchy, where the root node is called
//.."globalContext", a parent node is called "parentContext", and a child node
//..is called "localContext". A resource created in a given context is visible
//..in all localContexts (child nodes) of the context and any localContexts
//..of those localContexts. This means that a resource created in the
//..globalContext is also visible everywhrere else. However, if a localContext
//..contains a resource with an identical type and name as a global (or parent
//..level) resource, the parent level resource will be hidden, and in a search
//..the local resource is returned instead. Resources in a localContext are
//..never visible in it's parentContext or any sibling contexts.
//ResourceContext is not guaranteed to own any resources it manages. The
//..ownership of the managed resources is transferred to the object calling 
//..make(), makeNamed, find() or findNamed(), by returning a std::shared_ptr to
//..the requested object.
///////////////////////////////////////////////////////////////////////////////
class ResourceContext : public std::enable_shared_from_this<ResourceContext>
{
public:
    //The static member function "makeGlobalContext" creates a new resource
    //..management system. Calling it constructs and initializes a new
    //..ResourceContext object (independent of any existing ResourceContext
    //..objects), and returns a std::shared_ptr to the object.
    static std::shared_ptr<ResourceContext> makeGlobalContext()
    {
        return std::shared_ptr<ResourceContext>(
            new ResourceContext());
    }

    //"makeLocalContext" creates a new ResourceContext whose parent is the
    //..current context, and returns a std::shared_ptr to the new object.
    std::shared_ptr<ResourceContext> makeLocalContext()
    {
        if(globalContext_ == nullptr)
            return std::shared_ptr<ResourceContext>(
                new ResourceContext(shared_from_this(), shared_from_this()));
        else
            return std::shared_ptr<ResourceContext>(
                new ResourceContext(shared_from_this(), globalContext_));
    }

    //"getParentContext" returns a std::shared_ptr to the parent context of
    //..the current context. If the current context is the global context, a
    //..pointer to the current context is returned instead.
    std::shared_ptr<ResourceContext> getParentContext()
    {
        if(parentContext_ == nullptr)
            return shared_from_this();
        else
            return parentContext_;
    }

    //"getGlobalContext" returns a std::shared_ptr to the global context.
    std::shared_ptr<ResourceContext> getGlobalContext()
    {
        if(globalContext_ == nullptr)
            return shared_from_this();
        else
            return globalContext_;
    }

    //"makeNamed" searches for a resource with a matching type and ID. If a 
    //..matching resource is found in the current context, a shared_ptr to the 
    //..resource is returned, otherwise the constructor arguments passed to 
    //..this function are forwarded to the constructor of the resource class, 
    //..and a new resource object is instantiated.
    template<typename T, typename...Arg>
    std::shared_ptr<T> makeNamed(std::string id, Arg...ctorArgs)
    {
        int index = getIndex<T>();
        ResById &resourceMap = mapByIndex_[index];

        auto keyValPair = std::make_pair(id, std::weak_ptr<Resource>());

        auto &mappedPtr = resourceMap.insert(keyValPair).first->second;

        if(mappedPtr.expired())
        {
            auto newResource = std::make_shared<T>(*this, ctorArgs...);
            mappedPtr = std::static_pointer_cast<Resource>(newResource);
            return newResource;
        }
        else
            return std::static_pointer_cast<T>(mappedPtr.lock());
    }

    //"make" searches for a resource with a matching type. If a matching
    //..resource is found in the current context, a shared_ptr to the resource
    //..is returned, otherwise the constructor arguments passed to this
    //..function are forwarded to the constructor of the resource class, and
    //..a new resource object is instantiated.
    template<typename T, typename...Arg>
    std::shared_ptr<T> make(Arg...ctorArgs)
    {
        return makeNamed<T>(std::string(), ctorArgs...);
    }

    //"findNamed" returns a std::shared_ptr to a resource if a resource with a
    //..matching type and ID can be found in the current context or the current
    //..context's parent contexts, while returning a nullptr if no matching
    //..resource exists.
    template<typename T>
    std::shared_ptr<T> findNamed(std::string id)
    {
        int index = getIndex<T>();
        ResById &resourceMap = mapByIndex_[index];

        auto it = resourceMap.find(id);

        if(it != resourceMap.end())
        {
            if(!it->second.expired())
                return std::static_pointer_cast<T>(it->second.lock());
        }
        if(parentContext_ != nullptr)
            return parentContext_->find<T>(id);
        return nullptr;
    }

    //"find" returns a std::shared_ptr to a resource if a matching resource
    //..can be found in the current context or the current context's parent
    //..contexts, while returning a nullptr if no matching resource exists.
    template<typename T>
    std::shared_ptr<T> find()
    {
        return findNamed<T>(std::string());
    }

private:
    //The constructors are only intended to be called by
    //..ResourceContext::makeGlobalContext()
    ResourceContext() : parentContext_(nullptr), globalContext_(nullptr){}
    ResourceContext(std::shared_ptr<ResourceContext> parent,
                    std::shared_ptr<ResourceContext> global)
        : parentContext_(parent), globalContext_(global){}

    //Copy constructors and the assignment operator disabled
    ResourceContext(const ResourceContext&) = delete;
    ResourceContext& operator=(const ResourceContext&) = delete;

    //getIndex generates an index that is unique for each template
    //..specialization of the function, and resizes the vector of resource
    //..maps when needed. For internal use only.
    template<typename T>
    int getIndex()
    {
        static int id = -1;
        if(id < 0)
        {
            id = resourceTypeCount;
            resourceTypeCount++;
        }
        if(id >= (int)mapByIndex_.size())
            mapByIndex_.resize(resourceTypeCount);
        return id;
    }

    //The number of different resources currently supported.
    static int resourceTypeCount;

    //The resources managed by the current context, mapped by ID and resource
    //..type.
    typedef std::map<std::string, std::weak_ptr<Resource>> ResById;
    std::vector<ResById> mapByIndex_;

    //A ResourceContext object owns its parent context. This guarantees that
    //..the parent context does not go out of scope while a local context
    //..still exists.
    std::shared_ptr<ResourceContext> parentContext_;
    std::shared_ptr<ResourceContext> globalContext_;
};

int ResourceContext::resourceTypeCount = 0;

I would appreciate getting feedback on the following:

  • The use of C++11 features: smart pointers, (currently missing) move semantics, variadic templates and the auto keyword
  • Commenting style: Too little, too much, hard to follow?
  • Programming style: Structuring, legibility, possible mistakes, optimizations, code smell?
  • Most importantly, the general concept: It's implications on design complexity and performance
  • How to get rid of the ugly static variables? should I resort to RTTI (runtime type identification) in place of the current ID generator?
\$\endgroup\$
4
\$\begingroup\$

I can't answer all your questions, but here are some things you can improve or some guidelines you can follow:

  • Always pass std::shared_ptr by const reference. There are several reasons for this, some of them are explained in GOTW #91 by Herb Sutter.
  • In your function make, you problem want to take the parameters by universal reference and use std::forward instead of simply passing them. Therefore, you can tranform this:

    template<typename T, typename...Arg>
    std::shared_ptr<T> make(Arg... ctorArgs)
    {
        return makeNamed<T>(std::string(), ctorArgs...);
    }
    

    into that:

    template<typename T, typename...Arg>
    std::shared_ptr<T> make(Arg&&... ctorArgs)
    {
        return makeNamed<T>(std::string(), std::forward<Arg>(ctorArgs)...);
    }
    
  • Whenever possible, try to replace your insert functions by emplace functions, especially when you created an object to immediately store it. You can turn this:

    auto keyValPair = std::make_pair(id, std::weak_ptr<Resource>());
    auto &mappedPtr = resourceMap.insert(keyValPair).first->second;
    

    into that:

    auto &mappedPtr = resourceMap
        .emplace(id, std::weak_ptr<Resource>())
        .first->second;
    

    Technically speaking, you can go even further and use the std::pair piecewise construction to create your values directly in the std::pair directly in the std::map:

    auto &mappedPtr = resourceMap
        .emplace(
            std::piecewise_construct,
            std::forward_as_tuple(id),
            std::forward_as_tuple(std::weak_ptr<Resource>())
        ).first->second;
    
  • You could try to be consistent and use std::make_shared everywhere instead of using new from time to time. I know that std::make_shared has problems to interact with private or protected constructors. However, there are some workarounds that you can use if you want to consistently use std::make_shared whenever possible.

All of those tips are only guidelines to properly use c++11 and nothing is actually specific to your code. I unfortunately did not managed to really dive into your code and think about its logic. You will have to wait for another answer if you want to improve it too.

\$\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.