Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I, in C++, write a templated RAII wraper with custom function calls when functors are not an option?

I am currently working on an RAII system for OpenGl in standard¹ C++ 17 while making very heavy use of templates. Right now, the part of the system that I am working on is to bind and unbind various OpenGl objects via a common template with later using declarations to create easy aliases for each type. The following is a relevant excerpt of my header file that demonstrates the general technique:

template<typename T, void *bind, void *unbind, typename ... Args>
class RaiiGlBinding{
public:
    explicit RaiiGlBinding(const T &t, Args... args) : m_unbindArgs(std::make_tuple(t, args...)) { bind(t, args...); }
    ~RaiiGlBinding() { if(m_isDestructable) std::apply(unbind_typed, m_unbindArgs); }

private:
    static constexpr auto bind_Vaotyped = static_cast<void (*)(T, Args...)>(bind);
    static constexpr auto unbind_typed = static_cast<void (*)(T, Args...)>(unbind);
    bool m_isDestructable = true;
    std::tuple<T, Args...> m_unbindArgs;

};
Vao
namespace glraiidetail{
    inline void bindBuffer(GLuint buffer, GLenum target) { glBindBuffer(target, buffer); }
    inline void unbindBUffer(GLuint buffer, GLenum target) { glBindBuffer(target, 0); }
}

using RaiiBufferBinding = RaiiGlBinding<GLuint, &glraiidetail::bindBuffer, &glraiidetail::unbindBuffer>;

When I first attempted this class, I used non-void pointers in the template<> declaration (e.g. template<typename ... Args, void (*)(Args...)>), but this caused problems because 1) it is more difficult to manually specify Args, and 2) CLion told me that the variadic argument must be last.

I then moved the variadic argument to be last, solving both problems. This prevented the function argument from accessing the argument pack to unpack, however.

To solve this issue, I made the template arguments pointers to void and then casted them to more useful types in the class body, where both the function's address and the parameter pack are available. Due to my understanding of function pointers as no different than regular pointers, except where their address is to the machine code of the function in question, I figured that there would be no problem other than mildly compromised type safety² with this method.

Unfortunately, that proved to be false when my compiler wouldn't let me cast the function pointer to void*, explicitly or otherwise. After some research, I concluded that my previously apparent solution of pointers to void wasn't much of a solution because it is actually undefined behavior to cast a function pointer to void* in C++.

I can't use a functor because I want the users of my class to be able to get along without even knowing that it is a template class via my using declarations, but a functor would require each instantiation of it to pass an instance of the functor type.

As such, I ask the very smart people of stack overflow: How can I resolve this issue?

1: This means that I am strongly against any undefined behavior that will Probably Work™ because portability is important to me. 2: Although the casts themselves are unsafe, the compiler should stop compilation with an error in case of any issues when the template, and thus the cast, is instantiated. As I intended for users of my code to use it solely via the using declarations, such a possibly cryptic error is a very minor problem.

like image 594
john01dav Avatar asked Sep 08 '19 04:09

john01dav


1 Answers

Since you are using C++17, you can just use auto template parameters. You can add static assertions in the class body to ensure that the arguments are actually function pointers.

template <typename T, auto bind, auto unbind, typename... Args>
class RaiiGlBinding {
public:
    explicit RaiiGlBinding(const T &t, Args... args)
        : m_unbindArgs(std::make_tuple(t, args...)) {
        bind(t, args...);
    }
    ~RaiiGlBinding() {
        if (m_isDestructable)
            std::apply(unbind, m_unbindArgs);
    }

private:
    bool m_isDestructable = true;
    std::tuple<T, Args...> m_unbindArgs;
};

namespace glraiidetail {
inline void bindBuffer(GLuint buffer, GLenum target) {
    glBindBuffer(target, buffer);
}
inline void unbindBuffer(GLuint buffer, GLenum target) {
    glBindBuffer(target, 0);
}
} // namespace glraiidetail

using RaiiBufferBinding = RaiiGlBinding<GLuint, &glraiidetail::bindBuffer,
                                        &glraiidetail::unbindBuffer>;
like image 101
Henri Menke Avatar answered Oct 19 '22 00:10

Henri Menke