4

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.

  • std::allocate_shared lets you set an allocator so you can allocate however you want, you don't have to use the heap. – esoterik Jun 20 '18 at 22:23
  • 1
    I don't really think there are many benefits, but I was wondering if it would be frowned upon for whatever reason I am not aware of to use std::shared_ptr. My resource does not need to be allocated on the heap – Anonymous Person Jun 20 '18 at 22:40
  • 2
    It's pretty routine to use unique_ptr and shared_ptr for resources other than heap allocations. – Jerry Coffin Jun 20 '18 at 22:59
  • 4
    I'm more concerned about why you want shared ownership of a shader. Are you sure you want that? Because most graphics systems are sufficiently rigid that you don't just leave shaders lying around and truly owned by multiple users. They're resources, like textures, and you want their lifetimes to be controlled by a resource manager, not by how many objects in the scene use them. And if the resource manager is destroyed, then you should no longer be using the object; if you are, that represents a logic error. – Nicol Bolas Jun 20 '18 at 23:53
  • 1
    @NicolBolas Or you want the shader to hang around as long as something needs it and automatically be cleaned up when nothing references it any more. The simplest form of resource manager. – immibis Jun 21 '18 at 1:50
8

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

  • n.b. std::allocate_shared takes an allocator (and stores it) so one can control allocation however one wants, one doesn't have to use the heap. – esoterik Jun 21 '18 at 1:36
  • @esoterik: But since you have no idea how much memory it will allocate, you dramatically limit your options for non-heap (or heap-like) allocations. – Nicol Bolas Jun 21 '18 at 1:54
  • 1
    Personally I think the most idiomatic way to do this would be to create a Shader class which contains the GLuint and frees it in it's destructor, then you can just use a shared_ptr<Shader> if you want reference counting. – Sean Burton Jun 25 '18 at 12:46
0

When I was experimenting with this, I wrote a specific set of handle classes and ended up not using the standard smart pointers as I found them a little too heavyweight.

Full source here:

https://github.com/AlexAndDad/dungeon/blob/master/opengl/shader.hpp

I used the service/handle idiom to separate the concerns of handle behaviour (shared or unique) and functionality.

An excerpt here:  

/// Resource Service base implementation
/// When a resource_object manages its resources (e.g. copy, move, construction, destruction etc) it will
/// defer to its service object. The service object will be a concrete class derived from this class.
/// The reason for this is that the service can then handle the details around creating and deleting
/// GL handle objects and managing their lifetimes.
/// @tparam Derived is the concrete service class derived from this class
/// @tparam NativeType is the type used to store the underlying GL handle or collection of handles
template<typename Derived, typename NativeType = GLuint>
struct basic_resource_service;

template<class Derived>
struct basic_resource_service<Derived, GLuint> : notstd::stateless_service<Derived>
{
    using native_handle_type = GLuint;
    using implementation_type = native_handle_type;

    /// Determine whether the implementation is empty,i.e. does not represent a GL handle.
    bool empty(implementation_type const &impl) const noexcept
    {
        return not impl;
    }

    void invalidate(implementation_type& impl) const noexcept
    {
        impl = 0;
    }
};

/// A specialisation of basic_resource_service which handles a vector of GL handles
template<class Derived, class DataType>
struct basic_resource_service<Derived, std::vector<DataType>>
{
    using implementation_type = std::vector<DataType>;

    bool empty(implementation_type const &impl) const
    {
        return not impl.empty();
    }

    void invalidate(implementation_type& impl) const
    {
        impl.clear();
    }
};

struct shader_service : basic_resource_service<shader_service, GLuint>
{
    /// Construct a shader identity of a given type
    /// @param type is a gl shader type enum
    /// @return the gl id of a new shader
    ///
    static auto construct(shader_type type) -> implementation_type;

    /// Destroy a gl shader object if not zero
    /// @param impl is a reference to a shader id
    /// @pre impl contains either a valid shader object id or 0
    /// @post impl shall contain 0
    ///
    static auto destroy(implementation_type &impl) -> void;


    static std::size_t log_length(implementation_type const &impl);

    static std::string log(implementation_type const &impl);

    static std::size_t source_length(implementation_type const &impl);

    static std::string source(implementation_type const &impl);
};

struct shader_compilation_failed : std::runtime_error
{
    using std::runtime_error::runtime_error;
};

/// The representation of some kind of shader
struct shader : notstd::unique_handle<shader_service>
{
    shader(shader_type type)
        : notstd::unique_handle<shader_service>(std::make_tuple(type))
    {
    }

    template<class...Sources>
    shader(shader_type type, Sources &&...sources)
        : shader(type)
    {
        constexpr auto count = sizeof...(sources);

        const GLchar *sz_sources[] =
            {
                detail::to_gl_char(sources)...
            };

        const GLint lengths[] = {
            detail::get_gl_string_length(sources)...
        };

        glShaderSource(native_handle(), count, sz_sources, lengths);
        glCompileShader(native_handle());
        check_errors("shader::shader");
        if( not compiled())
        {
            throw shader_compilation_failed(log());
        }
    }

    /// Return the source code for this shader object, if it has any
    auto source() const -> std::string;

    /// Return the shader type
    auto type() const -> shader_type;

    /// Check whether the shader has compiled
    auto compiled() const -> bool;

    /// Return the log text associated with this shader
    auto log() const -> std::string;
};

struct fragment_shader : shader
{
    template
        <
            class String,
            std::enable_if_t
                <
                    not std::is_base_of<shader, std::decay_t<String>>::value
                > * = nullptr
        >
    fragment_shader(String &&str)
        : shader(shader_type::fragment, std::forward<String>(str))
    {

    }
};

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.