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;
}
1 Answer 1
ResourceOwner
:
We need to
#include <utility>
forstd::move
.Reset
might be more flexible taking its argument by valuevoid 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 thansize_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 countermutable
) to avoid allocating the reference counter for the first instance. If we specify aSharedResourceOwner
over a normalResourceOwner
, we probably need to use the reference counter anyway.
Some other points:
The
TextureData
class might prefer to settexture_id
to0
after callingglDeleteTextures
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. TheGet()
function can then access thedata
member through the pointer, instead of directly. When the data is invalid (i.e. immediately after construction, after moving from the owner, after callingReset()
), we can make that pointer null. This would be a decent way of catchingGet()
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 byconst&
instead of by value. (There are a lot of other places in the repository that do this).
glVertexAttribPointer
. \$\endgroup\$