3
\$\begingroup\$

I need a unique_ptr and shared_ptr like structure, but instead of pointers, I would like to store some kind of reference to a resource. It is usually just an int and maybe some attributes. I would like to avoid using pointers, so these objects can be kept on the stack.

I usually use these containers in my OpenGL projects or with some C libraries, where I call a C function to allocate a resource, and then get back an ID or opaque pointer. If I do not need that resource anymore, then I have to call the appropriate deallocator function. In this case, the deallocator is called by the container.

ResourceOwner:

template<typename T>
class ResourceOwner {

    T data;

protected:

    ResourceOwner() : data() {}

public:

    // Disable copy
    ResourceOwner(const ResourceOwner&) = delete;
    ResourceOwner &operator=(const ResourceOwner&) = delete;

    // Move constructor
    ResourceOwner(ResourceOwner&& other) noexcept : data(other.Release())
    {}

    // Move operator
    ResourceOwner &operator=(ResourceOwner&& other) noexcept
    {
        Reset(other.Release());
        return *this;
    }

    // Delete resource
    void Reset(T&& new_data = T())
    {
        data.Reset();
        data = std::move(new_data);
    }

    // Get raw data
    const T& Get() const {
        return data;
    }

    ~ResourceOwner()
    {
        Reset();
    }

private:

    // Release ownership
    T Release()
    {
        T ret = std::move(data);
        data = T();
        return ret;
    }

};

The reference counter is a pointer in the SharedResourceOwner, but it is only created, when the second instance is created. The resource reference is not a pointer, because it is immutable, so it can be copied between shared owners.

SharedResourceOwner:

template<typename T>
class SharedResourceOwner {

    // number of owners except this
    mutable size_t* reference_counter = nullptr;
    // raw resource data
    T data = T();

protected:

    SharedResourceOwner() {}

public:

    ~SharedResourceOwner() {
        Reset();
    }

    // Copy constructor
    SharedResourceOwner(const SharedResourceOwner& other) noexcept {
        other.IncreaseCounter();

        reference_counter = other.reference_counter;
        data = other.data;
    }

    // Copy operator
    SharedResourceOwner &operator=(const SharedResourceOwner& other) noexcept {

        if (&other != this) {
            Reset();

            other.IncreaseCounter();

            reference_counter = other.reference_counter;
            data = other.data;
        }

        return *this;
    }

    // Move constructor
    SharedResourceOwner(SharedResourceOwner &&other) noexcept
    {
        reference_counter = other.reference_counter;
        data = std::move(other.data);

        other.reference_counter = nullptr;
        other.data = T();
    }

    // Move operator
    SharedResourceOwner &operator=(SharedResourceOwner &&other) noexcept
    {
        Reset();

        reference_counter = other.reference_counter;
        data = std::move(other.data);

        other.reference_counter = nullptr;
        other.data = T();

        return *this;
    }

    // delete resource
    void Reset(T&& new_data = T()) {
        if (reference_counter == nullptr) {
            data.Reset();
        }
        else if (*reference_counter == 0) {
            delete reference_counter;
            data.Reset();
        }
        else {
            (*reference_counter)--;
        }

        reference_counter = nullptr;
        data = std::move(new_data);
    }

    // Get raw data
    const T& Get() const {
        return data;
    }

private:

    void IncreaseCounter() const {
        if (reference_counter == nullptr) {
            reference_counter = new size_t(0);
        }

        reference_counter++;

    }

};

The type T is a "ResourceReference" type. It is immutable inside the container. The content of the container may be reset or replaced, but its attributes cannot be changed, therefore you are only able to get a constant reference to it with Get()


Example class

An example type T looks like this:

struct TextureData {
    GLuint texture_id = 0;
    GLenum target = GL_TEXTURE_2D;

    TextureData() {}
    TextureData(GLuint texture_id) : texture_id(texture_id) {}

    void Reset() {
        if (texture_id > 0) {
            glDeleteTextures(1, &texture_id);
        }
    }
};

The contained classes are really simple, they only have some data members, and Reset is not expected to throw exception.

An example container:

class Texture : public ResourceOwner<TextureData> {

public:

    Texture() : ResourceOwner() {}

    GLuint GetId() const {
        return Get().texture_id;
    }

    // static helper function which returns a new image loaded from the path
    // defined in Texture.cpp
    static Texture FromFile(std::string path);

};

Usage of the example class

app.hpp:

#include "Texture.hpp"

class App {

    Texture myTexture;

    // ...

    void Init();
    void Render();

    // ...

}

app.cpp:

void App::Init() {

    myTexture = Texture::FromFile("example.jpg");

    // ...

}

void App::Render() {

    // ...

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(texture.Get().target, myTexture.GetId());
    SetUniform(tex_uniform_id, 0);

    // ...

}

Example Texture creation:

Texture createTextureFromData(
    std::vector<uint8_t> data,
    GLsizei width = 0,
    GLsizei height = 0
) {

    Texture texture
    TextureData texture_data;

    // Generate OpenGL texture
    glGenTextures(1, &texture_data.texture_id);

    glBindTexture(GL_TEXTURE_2D, texture_data.texture_id);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data.data());

    // Generate mipmap
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glGenerateMipmap(GL_TEXTURE_2D);

    //
    texture.Reset(std::move(texture_data));
    return texture;

}
\$\endgroup\$
  • \$\begingroup\$ Can you provide an example demonstrating the usage of the functionalities? (@chux-ReinstateMonica Done.) \$\endgroup\$ – L. F. Jan 21 at 9:24
  • \$\begingroup\$ I provided some examples \$\endgroup\$ – Adam Jan 21 at 9:41
  • \$\begingroup\$ Can you make it a small but complete example? The example you presented in its current form is not meaningfully reviewable. We can only review real, working code. Thank you! \$\endgroup\$ – L. F. Jan 21 at 10:26
  • \$\begingroup\$ I'm curious to know if you've implemented VBOs yet, and how you handle passing the buffer component count and component type to glVertexAttribPointer. \$\endgroup\$ – user673679 Jan 21 at 12:45
  • \$\begingroup\$ @user673679 Yes, I implemented containers for many OpenGL resources. But I am looking for feedbacks for the two ResourceOwner classes. Here you can see the other classes too with an example project: github.com/K-Adam/GLWrapper \$\endgroup\$ – Adam Jan 21 at 13:24
2
\$\begingroup\$

ResourceOwner:

  • We need to #include <utility> for std::move.

  • Reset might be more flexible taking its argument by value void Reset(T new_data = T()), so we can copy into it instead of moving.

  • It might be useful to have a value constructor: ResourceOwner(T data);.

SharedResourceOwner:

  • Again, #include <utility>.

  • Use std::size_t rather than size_t for C++.

  • Use the constructor initializer list to initialize member variables, instead of assigning them in the body of the constructor.

  • We could provide a Swap() function, allowing us to implement copy and move assignment more easily:

    void Swap(SharedResourceOwner& other)
    {
        using std::swap;
        swap(reference_counter, other.reference_counter);
        swap(data, other.data);
    }
    
    SharedResourceOwner &operator=(const SharedResourceOwner& other) noexcept
    {
        SharedResourceOwner temp(other);
        Swap(temp);
        return *this;
    }
    
    SharedResourceOwner &operator=(SharedResourceOwner &&other) noexcept
    {
        SharedResourceOwner temp(std::move(other));
        Swap(temp);
        return *this;
    }
    

    (We could do the same for ResourceOwner).

  • Again, Reset() can take its parameter by value.

  • I'm not sure it's worth the extra complication (extra logic in Reset(), and making the counter mutable) to avoid allocating the reference counter for the first instance. If we specify a SharedResourceOwner over a normal ResourceOwner, we probably need to use the reference counter anyway.


Some other points:

  • The TextureData class might prefer to set texture_id to 0 after calling glDeleteTextures for added safety.

  • Just resetting the owned data to a default constructed value is potentially dangerous. We have no easy way to ensure that Get() isn't called when the data is invalid.

    As well as storing T data; by value in the resource owner classes we could store a pointer to that same data member. The Get() function can then access the data member through the pointer, instead of directly. When the data is invalid (i.e. immediately after construction, after moving from the owner, after calling Reset()), we can make that pointer null. This would be a decent way of catching Get()s when the data is invalid.

    Alternatively, storing the data as std::optional<T> data; could do the same thing with less hassle.

  • This looks like it copies the path string unnecessarily: static Texture FromFile(std::string path); we should pass it by const& instead of by value. (There are a lot of other places in the repository that do this).

| improve this answer | |
\$\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.