University of Wales Trinity Saint David
ACCB5017 - Games Architecture and Mechanics

RAII and Smart Pointers

Overview

This lab builds a small AssetManager that loads and releases named game resources, such as textures or audio buffers, without a single call to delete anywhere in its own code. Resource represents one loaded asset that acquires an "expensive" handle in its constructor and releases it in its destructor, the pattern of tying a resource's lifetime to an object's lifetime known as RAII. You will complete AssetManager::load() and AssetManager::unload() so every Resource is owned by a std::unique_ptr stored in a map, then run a demo that loads three assets, releases one early, and lets the manager go out of scope, to see every remaining resource release itself automatically in the order the map destroys its entries.

Learning Objectives

By the end of this lab you will be able to:

Concept

A class that loads an expensive resource, such as a GPU texture or an audio buffer, with a raw pointer has to remember to release it later with a matching delete. Forget the delete and the resource leaks. call it twice, from two objects that both think they own the same handle, and the program crashes. The more places a resource can be released from, an early return, a thrown exception, a manual unload function, the easier it is to miss one of them.

RAII, Resource Acquisition Is Initialization, solves this by tying a resource's lifetime directly to an object's lifetime instead of relying on a programmer to call the right cleanup function at the right time. Resource's constructor acquires its handle, and its destructor releases it, so as long as a Resource is reachable through the stack, a member, or a smart pointer, the compiler guarantees the destructor runs when that object's scope ends, however that scope ends. Resource also deletes its copy constructor and copy assignment operator, so two Resource objects can never both believe they own the same handle, and defines a move constructor and move assignment operator instead, so ownership can still be transferred safely from one object to another.

AssetManager applies this at a larger scale. it stores each Resource inside a std::unique_ptr, a smart pointer that owns exactly one object and destroys it automatically when the unique_ptr itself is destroyed. std::make_unique(...) constructs a Resource and hands sole ownership to the returned unique_ptr in one step, and erasing that unique_ptr from its container, whether explicitly or because the container itself is destroyed, releases the Resource immediately.

Tasks

Task 1 - Reading Resource: acquire in the constructor, release in the destructor

Open Resource.h and Resource.cpp. Both files are already complete. there is nothing to implement in this task. Read through the constructor, the destructor, and the copy and move members, and notice how each one treats handle.

  1. Note that the constructor prints an [acquire] message and assigns handle a fresh value, standing in for whatever a real API would hand back for a loaded texture or sound buffer.
  2. Note that the destructor only prints a [release] message when handle != -1, and that the move constructor and move assignment operator both set other.handle = -1 on the object being moved from.
  3. Note that the copy constructor and copy assignment operator are declared as = delete, so Resource a = b; fails to compile. before moving on, predict why allowing that line to compile would be dangerous once two Resource objects both held the same handle.

Success condition: you can state, in your own words, why setting other.handle = -1 inside the move constructor stops the moved-from object's destructor from releasing a handle it no longer owns.

Resource.h
#pragma once
#include <string>

// Simulates a single loaded game asset (e.g. a texture or sound buffer)
// that owns an "expensive" handle to some external system (GPU memory,
// an audio device buffer, a file handle...). There's no SFML here, so
// handle just stands in for whatever id a real API would hand back.
//
// The whole point of this class: acquire the resource in the constructor,
// release it in the destructor. As long as every Resource is reachable
// through RAII (stack, member, or a smart pointer), it releases itself -
// nobody has to remember to call an "unload" function.
class Resource {
public:
    Resource(const std::string& name, const std::string& filepath);
    ~Resource();

    // Non-copyable: two Resource objects must never believe they own the
    // same handle, or both destructors would try to release it.
    Resource(const Resource&) = delete;
    Resource& operator=(const Resource&) = delete;

    // Movable: ownership of the handle can be transferred safely.
    Resource(Resource&& other) noexcept;
    Resource& operator=(Resource&& other) noexcept;

    const std::string& getName() const { return name; }
    int getHandle() const { return handle; }

private:
    std::string name;
    std::string filepath;
    int handle; // -1 means "this object no longer owns a resource"
};
Resource.cpp
#include "Resource.h"
#include <iostream>
#include <utility>

namespace {
    int nextHandle = 1;
}

Resource::Resource(const std::string& name, const std::string& filepath)
    : name(name), filepath(filepath), handle(nextHandle++) {
    std::cout << "  [acquire] loading '" << name << "' from " << filepath
        << " -> handle " << handle << std::endl;
}

Resource::~Resource() {
    if (handle != -1) {
        std::cout << "  [release] unloading '" << name << "' (handle " << handle << ")" << std::endl;
    }
}

Resource::Resource(Resource&& other) noexcept
    : name(std::move(other.name)), filepath(std::move(other.filepath)), handle(other.handle) {
    other.handle = -1; // moved-from object no longer owns anything
}

Resource& Resource::operator=(Resource&& other) noexcept {
    if (this != &other) {
        if (handle != -1) {
            std::cout << "  [release] unloading '" << name << "' (handle " << handle << ")" << std::endl;
        }
        name = std::move(other.name);
        filepath = std::move(other.filepath);
        handle = other.handle;
        other.handle = -1;
    }
    return *this;
}

Task 2 - AssetManager: owning every Resource through a unique_ptr

Open AssetManager.h, already fully declared, and AssetManager.cpp. Complete the two functions marked TODO, load() and unload(), so the manager never calls new or delete directly on a Resource.

  1. Implement AssetManager::load(). construct the new resource with std::make_unique(name, filepath), store the result in the resources map under name, then return a reference to it. assigning over an existing entry for the same name should destroy the previous Resource automatically, since assigning a new value to a unique_ptr releases whatever it previously owned first.
  2. Implement AssetManager::unload(). find name in resources. if it exists, print "explicitly unloading '" << name << "'..." and then erase() that entry, which destroys the unique_ptr and releases its Resource immediately.
  3. Leave get(), has() and count() unchanged. get() already returns a non-owning raw pointer with it->second.get(), borrowed from the unique_ptr without taking ownership.

Success condition: the project compiles, and calling load() for three different names followed by unload() for one of them prints exactly one [acquire] per load and exactly one [release] for the unloaded name, with no delete written anywhere in AssetManager.cpp.

AssetManager.h
#pragma once
#include <memory>
#include <string>
#include <unordered_map>
#include "Resource.h"

// The asset-loading logic isn't the point here - it's that every Resource
// is owned by a std::unique_ptr, so the manager's own code never has to
// call "delete" or remember to clean anything up.
class AssetManager {
public:
    // Loads (or reloads) a resource under the given name.
    Resource& load(const std::string& name, const std::string& filepath);

    // Non-owning access to an already-loaded resource. The manager still
    // owns it; this pointer must not outlive the manager (or the entry).
    Resource* get(const std::string& name);

    bool has(const std::string& name) const;

    // Releases one resource immediately instead of waiting for the
    // manager's own destructor to run.
    void unload(const std::string& name);

    size_t count() const { return resources.size(); }

private:
    std::unordered_map<std::string, std::unique_ptr<Resource>> resources;
};
AssetManager.cpp
#include "AssetManager.h"
#include <iostream>

Resource& AssetManager::load(const std::string& name, const std::string& filepath) {
    // TODO: construct the new resource with std::make_unique<Resource>(name,
    // filepath), store it in resources[name], and return a reference to it.
    // Assigning over an existing entry for the same name should destroy
    // the previous Resource first, since assigning a new value to a
    // unique_ptr releases whatever it previously owned before taking the
    // new one.
}

Resource* AssetManager::get(const std::string& name) {
    auto it = resources.find(name);
    if (it == resources.end()) {
        return nullptr;
    }
    return it->second.get(); // borrow a raw pointer; ownership stays with the manager
}

bool AssetManager::has(const std::string& name) const {
    return resources.find(name) != resources.end();
}

void AssetManager::unload(const std::string& name) {
    // TODO: find name in resources. If it exists, print
    // "explicitly unloading '" << name << "'..." and then erase() that
    // entry, which destroys the unique_ptr and releases its Resource
    // immediately.
}

Task 3 - Running the demo: automatic cleanup at scope exit

Open RAIIAssetManager.cpp, already complete. it does not need editing. Read through main() to see how it exercises the AssetManager you completed in Task 2, then build and run the project.

  1. Note that assets is constructed inside an inner scope, and that three resources are loaded through it with load().
  2. Note that unload("music") releases one resource early, before the inner scope ends, printing its own [release] message immediately rather than waiting.
  3. Note the closing brace of the inner scope. assets is destroyed there, and its member resources map is destroyed along with it, which in turn destroys every remaining unique_ptr still stored inside it.

Success condition: the program prints an [acquire] line for each of the three loads, one explicit [release] line for "music" from unload(), then two more [release] lines for "player" and "terrain" printed automatically when the inner scope closes, with the final line confirming nothing is left to clean up manually.

RAIIAssetManager.cpp
#include <iostream>
#include "AssetManager.h"

// Demonstrates RAII / smart-pointer resource management using a trimmed-
// down version of PlatformerSFML's AssetManager (see Resource.h/.cpp and
// AssetManager.h/.cpp). Every asset is owned by a std::unique_ptr inside
// the manager, so:
//   - loading an asset acquires it (Resource's constructor runs)
//   - the manager's own code never calls "delete" anywhere
//   - a resource is released the instant its owning unique_ptr is
//     destroyed -- via an explicit unload(), an early erase(), or the
//     manager itself going out of scope
int main() {
    std::cout << "--- entering AssetManager scope ---" << std::endl;
    {
        AssetManager assets;

        assets.load("player", "assets/player.png");
        assets.load("terrain", "assets/terrain.png");
        assets.load("music", "assets/theme.ogg");

        std::cout << "loaded " << assets.count() << " resources" << std::endl;

        if (Resource* player = assets.get("player")) {
            std::cout << "using '" << player->getName() << "' (handle "
                << player->getHandle() << ")" << std::endl;
        }

        // Releasing one resource early -- no manual "delete", just erase
        // it from the map and the unique_ptr's destructor does the rest.
        assets.unload("music");

        std::cout << "--- leaving AssetManager scope ---" << std::endl;
    } // <- 'assets' is destroyed here; every remaining unique_ptr releases
      //    its Resource automatically.

    std::cout << "--- scope exited: nothing left to clean up manually ---" << std::endl;
    return 0;
}

Solution

Try the tasks above before revealing the solution.