4
\$\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;
}
asked Jan 20, 2020 at 23:10
\$\endgroup\$
6
  • \$\begingroup\$ Can you provide an example demonstrating the usage of the functionalities? (@chux-ReinstateMonica Done.) \$\endgroup\$ Commented Jan 21, 2020 at 9:24
  • \$\begingroup\$ I provided some examples \$\endgroup\$ Commented Jan 21, 2020 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\$ Commented Jan 21, 2020 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\$ Commented Jan 21, 2020 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\$ Commented Jan 21, 2020 at 13:24

1 Answer 1

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).

answered Jan 22, 2020 at 10:14
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.