Welcome to the tutorial about the Resources module of the Thor Library. Resources are heavyweight objects that can be loaded from the application, such as images, fonts, sound files, etc. In SFML, there exist six classes with resource semantics:
sf::Music is a special case; it behaves quite distinctly from the rest. Music is streamed and thus not completely loaded into memory. It has no real data, so shared access makes not much sense.
The Thor Resources module provides an interface that simplifies loading and access to these resources. Basically, the module consists of three widely independent parts:
Keys are IDs to access the resources. They contain loading information for the corresponding resource. A resource key for an
sf::Image which should be loaded from a file “image.jpg” is declared like this (given we write
using thor::Resources::ImageKey; for simplicity):
ImageKey key = ImageKey::FromFile("image.jpg");
These static factory methods indirect to the loading functions in
LoadFromMemory(), and they have the same signature. This relation applies for the other resource classes in SFML, too.
As mentioned, resource keys represent identifiers for resource access. Keys that are initialized identically refer to the same resource. Analogously, keys being initialized with different parameters (or even different factory functions) are always distinct, which means they refer to different resources. Note that resource keys do neither store nor reference the resource. The only thing they have is the abstract knowledge about how to load resources and how to differ between them.
The class template
thor::ResourceManager is the core of the resource management system. It maps keys to resources and returns pointers to access them. But before, it has to load the resources into memory, what can be done using the resource keys:
ImageKey key = ImageKey::FromFile("image.jpg"); thor::ResourceManager<sf::Image> mgr; mgr.Acquire(key);
Acquire() function loads the resource according to the way in which the resource key was initialized. In the upper example, the image is loaded from a file called “image.jpg”. The next time we acquire the image using the same key, the
ResourceManager recognizes that the corresponding resource has already been allocated and does not load it again. But when we use a different key (initialized with other parameters or another factory function), we address another resource, therefore it is not cached. To make it clear:
thor::ResourceManager<sf::Image> mgr; mgr.Acquire(key); // Loads a new resource mgr.Acquire(key2); // Loads a new resource mgr.Acquire(key); // No loading (resource already stored)
In case a resource cannot be loaded (e.g. wrong filename), the method
Acquire() throws an exception of type
thor::ResourceManager does actually not own the resources. It rather acts as a cache with the knowledge about the resources it has already loaded (and how they have been loaded). The resource manager does automatically free resources in its destructor, but only those which aren't referenced from the outside anymore.
We have now seen how to load resources, but how can we actually use them? Here comes the third component of the Resources module into play: The class template
ResourcePtr. This smart-pointer offers a safe and elegant way to access the resources as soon as they are loaded. It is returned by the
thor::Resources::ImageKey key = …; thor::ResourceManager<sf::Image> mgr; thor::ResourcePtr<sf::Image> image = mgr.Acquire(key);
The smart pointer
ResourcePtr has the following properties:
- Pointer-like behavior through overloaded dereferencing operators
->to access the resource class.
- Shared-ownership semantics. If you copy
ResourcePtrinstances, you get multiple smart pointers that point to the same resource. The instances are lightweight objects, they can be passed around or stored in STL containers without the high cost of copying the resource itself.
- Strong reference semantics. As long as a
ResourcePtrrefers to a resource, the latter cannot be released. This prevents accidental destruction of resources that are in use. Even when the
ResourceManagerthat loaded the resource is destroyed, remaining
ResourcePtrinstances are not affected.
When you want to access loaded resources without possibly allocating new ones,
Search() is the function of your choice. It never loads resources. In case nothing is found, a null pointer is returned.
When you allocate resources, you also have to deallocate them. Or maybe not you alone, the
ResourceManager helps you doing this task. To allow reasonable deallocation, the resource class must support RAII, hence its destructor must take care of cleaning its own resources up. But this is normally no problem when you program in C++. Every SFML resource class does this automatically.
Resources stored in a resource manager are released in the destructor
~ResourceManager() – unless you release them before, or unless you still need them. Without further configurations, resources are kept in memory until you explicitly release them. This behavior is quite meaningful, as it saves time to reallocate resources that are temporarily unused. On the other hand, there are situations which require a preferably low resource allocation. Here, resources are released as soon as they become unused. The Thor library supports both release strategies; they reside as
AutoRelease in namespace
Now you still don’t know how to release resources! Well, that’s simple, just call the
… // as always mgr.Release(key);
If the corresponding resource is not used at the moment of the call, it is immediately released. Otherwise, things get more complex. As mentioned above,
ResourcePtr instances keep their resources alive, so the resource is not released. However, the
ResourceManager will take care of releasing it ASAP – this happens the moment that the last
ResourcePtr loses ownership of the resource.
Const correctness [advanced]
It is often desirable that resources are not altered after they have been initialized.
ResourcePtr is powerful enough to imitate the const-correct behavior of raw pointers. That is to say, you can add const qualifiers, but not remove them.
thor::ResourcePtr<sf::Image> ptr; thor::ResourcePtr<const sf::Image> cptr; ptr = ptr; // ok cptr = cptr; // ok cptr = ptr; // ok, adding const-qualifier ptr = cptr; // not okay, removing const-qualifier
So when you use pointers to constant resources, you prevent accidental modification through
ResourcePtr. If you are very radical, you can even declare the template argument of
ResourceManager const! Like this, you have no possibility to change resources once they have been loaded.
thor::ResourceManager<const sf::Image> mgr; thor::ResourcePtr<const sf::Image> cptr = mgr.Acquire(key); // ok thor::ResourcePtr<sf::Image> ptr = mgr.Acquire(key); // not ok
Different management strategies [advanced]
The resource manager allows tweaking some behavior. This concerns the following actions, which concern
Acquire() calls in the future:
- When a resource cannot be acquired, throw exception or return null pointer? This can be configured via
- When the last resource pointer loses ownership of a resource, automatically release it or only release on explicit request? Use
SetReleaseStrategy()to choose the reaction.
Custom resources [advanced]
Having your own resource class, the following steps are required to make it compatible with
- Your resource class shall support RAII, but neither copyability nor default-constructibility is required.
- You need to define a key class that conforms the requirements in the documentation (second template parameter of