Implementing Reference Counting vs Using shared_ptr in C++

cc++11resources

In an OpenGL application that I am writing, I want to have a simple shader class to wrap the OpenGL shader handle. Ultimately, I want this shader class to behave very similarly to a shared_ptr in c++ (that is, keep a reference count and free the resource when no references are left). While it is relatively trivial to implement this reference counting from scratch, I was wondering if it is considered a better design choice to instead use std::shared_ptr with a custom deleter to free up the resource.

The main source of my doubt is the fact that it might be considered unconventional due to the fact that (as far as I know), creating an OpenGL shader program handle does not actually involve heap memory (which is what shared_ptr takes care of) but I still feel that it could apply here because I want this resource to be handled very similarly to how heap memory would be handled. The purpose of this question is basically to seek the opinion of others as to whether this actually is unconventional, because I do not know.

Also, although I used shaders as an example, the same question also applies to textures and buffers in OpenGL as they also must be allocated and freed.

Best Answer

unique_ptr<T, D> is actually specially designed to be able to work with more arbitrary handle-like types. I spelled out the template name fully because D is the key here. Normally unique_ptr<T, D>::get() returns a T*. That is the default, but this can be overridden by D: the deleter type.

If the deleter type has a pointer alias (D::pointer is legal syntax), then unique_ptr<T, D>::get() will return that type. This allows you to have something like unique_ptr<GLuint, gl::program_deleter>, where gl::program_deleter::pointer is of type int.

I bring all of this up because shared_ptr cannot do this. unique_ptr<T, D> gets away with it because the deleter is actually part of the unique_ptr type itself. By contrast, while shared_ptr's constructors can take a deleter, the only thing that deleter function can do is delete the memory.

So shared_ptr<GLuint>::get() will always return a GLuint*. This means that, if you want to use shared_ptr as some kind of shared handle type, that type must be dynamically allocated in some way. You may not be using the global heap, but it cannot just store and return integer either. shared_ptr<T> always contains a T*.

So no matter what, you're going to have to manage GLuint*s if you want to use shared_ptr's reference counting machinery. Yes, the deleter can be used to call glDeleteProgram or whatever you want, but the shared_ptr<GLuint> will still be storing a GLuint*.

creating an OpenGL shader program handle does not actually involve heap memory

OK, let's forget for a moment that by creating an OpenGL object, the driver almost certainly heap allocated some memory. Let's look just at what you have to do.

By creating a shared_ptr that owns some storage, something will be allocated. Namely, the shared block that manages the shared_ptr's reference count. There's no getting around that. So if you want to use shared_ptr's reference counting infrastructure, you're going to allocate from somewhere.

So the most idiomatic way to do this is to just heap allocate a GLuint and use a special deleter that destroys the OpenGL object and deallocates the integer. It's not pretty and it's kind of wasteful, but it's hardly terrible. And if you use make_shared, you can make things pretty compact in terms of allocations.


Now, you can avoid this allocation by cheating. You can do this:

GLuint program = glCreateProgram();
shared_ptr<GLuint> sp(reinterpret_cast<GLuint*>(program), ProgramDeleter);

So here, we're taking an integer and casting it to a pointer value, to be stored within the shared_ptr. When you need to use it, you have to reverse the cast to recover the integer value.

But judge the following code for yourself:

glProgramUniform1i(reinterpret_cast<GLuint>(sp.get()), val);

Does that look like something you want to do frequently? Does it look like something you want to read frequently? Does that look like something that someone else will easily understand what's going on?

Not only that, you can never use *sp to get the value, since the pointer value is the value in question.

Oh, and the reference counting control block still gets heap allocated, so it's not like you prevent allocating memory or something.

This is not idiomatic C++.