Using the std::weak_ptr as a handle (C++)

> Coding, hacking, computer graphics, game dev, and such...
User avatar
fips
Site Admin
Posts: 170
Joined: Wed Nov 12, 2008 9:49 pm
Location: Prague
Contact:

Using the std::weak_ptr as a handle (C++)

Post by fips »

Although I'm a committed fan of handle-based systems when it comes to managing resources in the context of game development, I wanted to try another approach this time, something more resembling the contemporary C++ style. All that effort just for the sake of curiosity to see how it would compete. So I involved the std::shared_ptr and std::weak_ptr both used in a somehow inverse scenario that offers an explicit resource management through create/destory() functions. Instead of typical handles, the system emits std::weak_ptrs as resource identifiers, which brings some advantages. The key thing here, compared to handles, is the ease of debugging it brings, especially when a decent debugger is available, which Visual C++ 2012 that I use surely has! The debugger certainly understands the standard C++ constructs better than a custom system of handles, and thus can provide some fancy insights into the internals. See the code + some screenshots below to get the idea:

VIEW THE CODE BELOW IN FULL-SCREEN (weak_ptr_handle_sample.cpp)

Code: Select all

/*
(c) 2013 +++ Filip Stoklas, aka FipS, http://www.4FipS.com +++
THIS CODE IS FREE - LICENSED UNDER THE MIT LICENSE
ARTICLE URL: http://forums.4fips.com/viewtopic.php?f=3&t=1066
*/

#include <string>
#include <vector>
#include <memory>
#include <algorithm>
#include <iostream>
#include <cassert>

size_t g_num_allocs = 0;

/// Overloaded global new, just to see allocations.
void * operator new (size_t size)
{
    void *ptr = ::malloc(size);
    std::cout << "+ new: 0x" << ptr << ", " << size << " byte(s)\n";
    ++g_num_allocs;
    return ptr;
}

/// Overloaded global delete, just to see deallocations.
void operator delete (void *ptr)
{
    std::cout << "- delete: 0x" << ptr << "\n";
    ::free(ptr);
}

/// A Resource that can by instantiated only through the Manager.
class Resource
{
 public:

    const std::string & name() const { return _name; }

 private:

    friend class Manager; ///< Resource factory.

    Resource();
    Resource(const std::string &name) : _name(name) {}

    std::string _name;
};

typedef std::shared_ptr<Resource> Resource_holder;
typedef std::weak_ptr<Resource> Resource_handle;

/// A helper that asserts on an attempt to lock an expired weak_ptr.
template <typename T>
std::shared_ptr<T> safe_lock(std::weak_ptr<T> w)
{
    std::shared_ptr<T> p(w.lock());
    assert(p && "An expired weak_ptr detected!");
    return p;
}

/// A Manager that explicitly controls the lifetime of Resources.
class Manager
{
 public:

    Manager() {}

    /// Returns a handle (weak pointer) to the newly created Resource.
    Resource_handle create_resource(const std::string &name)
    {
        _resources.emplace_back(Resource_holder(new Resource(name)));
        return _resources.back();
    }

    /// Destroys an existing Resource using the provided handle (weak pointer).
    void destroy_resource(Resource_handle r)
    {
        assert(std::find(begin(_resources), end(_resources), safe_lock(r)) != end(_resources));
        _resources.erase(remove(begin(_resources), end(_resources), safe_lock(r)), end(_resources));
    }

 private:

    Manager(const Manager &); // no copy
    Manager & operator = (const Manager &); // no assignment

    std::vector<Resource_holder> _resources;
};

int main()
{
    //Resource r("A"); // doesn't compile!
    //Resource *r = new Resource("A"); // doesn't compile!

    Manager mgr;

    std::cout << "<X>\n";

    Resource_handle r1 = mgr.create_resource("Resource A");
    std::cout << safe_lock(r1)->name() << "\n";

    std::cout << "<Y>\n";
    Resource_handle r2 = r1;
    std::cout << safe_lock(r2)->name() << "\n";
    std::cout << "<Y>\n";

    mgr.destroy_resource(r1);

    //m.destroy_resource(r2); // asserts! (double destroy)
    //std::cout << safe_lock(r1)->name() << "\n"; // asserts! (already destroyed)

    std::cout << "<X>\n";
    std::cout << "Total number of allocations: " << g_num_allocs << "\n";

    return 0;
}

// Compiled under Visual C++ 2012 (debug), output:
// + new: 0x00816470, 8 byte(s)
// <X>
// + new: 0x0081C3E8, 8 byte(s)
// + new: 0x0081C888, 28 byte(s)
// + new: 0x0081C500, 8 byte(s)
// + new: 0x0081B380, 16 byte(s)
// + new: 0x0081C378, 8 byte(s)
// - delete: 0x0081C3E8
// Resource A
// <Y>
// Resource A
// <Y>
// - delete: 0x0081C500
// - delete: 0x0081C888
// <X>
// Total number of allocations: 6
// - delete: 0x0081B380
// - delete: 0x0081C378
// - delete: 0x00816470
Image

Image

Besides some obvious pros, the approach also has a bunch of cons. There's some overhead caused by additional memory allocations, and more importantly the shared ref. counter that the smart pointers use internally needs to be protected in a multi-threaded environment (each time a std::weak_ptr gets copied or converted to std::shared_ptr, then when the std::shared_ptr goes out of the scope), which might prove quite expensive. Please note that typical handles don't suffer from this. Handles are also semantically clearer as they serve as pure identifiers that encourage one to think twice before invoking an operation through them.

Image

Image