C++ Defer

2024-03-24

 

In the development of C++ applications, especially those interfacing with C-style APIs, managing resources efficiently while ensuring exception safety can be challenging. To address this, I wrote a Zig-inspired defer construct, offering a practical solution for scenarios where implementing a full RAII wrapper is deemed excessive.

The defer construct is designed as follows:

namespace stdex
{
    class defer
    {
    public:
        defer(const std::function<void ()>& fun) noexcept
        : fun(fun) {}

        ~defer()
        {
            if (fun)
            {
                fun();
            }
        }

        void cancel() noexcept
        {
            fun = {};
        }

    private:
        std::function<void ()> fun;

        defer(const defer&) = delete;
        defer& operator = (const defer&) = delete;
    };
}

This implementation ensures that resources are cleaned up automatically when the scope is exited, regardless of whether the exit is due to normal completion or an exception. To use the defer construct, a defer object is declared in the same scope as the resource it is managing. Upon scope exit, the specified lambda function is executed, performing the necessary cleanup.

For example, in the context of OpenGL shader management:

OpenGLShader::OpenGLShader(const std::map<ShaderType, std::string>& shaders)
{
    auto vertex_id   = compile_shader(GL_VERTEX_SHADER,   shaders.at(ShaderType::VERTEX));
    auto d1 = stdex::defer([&](){ glDeleteShader(vertex_id); });

    auto fragment_id = compile_shader(GL_FRAGMENT_SHADER, shaders.at(ShaderType::FRAGMENT));
    auto d2 = stdex::defer([&](){ glDeleteShader(fragment_id); });

    program_id       = link_program({vertex_id, fragment_id});
}

There may be situations where the managed resource needs to be returned or kept alive beyond the current scope. In such cases, the defer construct offers a cancel method, allowing the deferred function to be bypassed and preventing it from executing.

glm::uint compile_shader(GLenum type, const std::string& code)
{
    auto shader = glCreateShader(type);
    auto d = stdex::defer([&](){ glDeleteShader(shader); });

    auto c_code = code.data();
    auto c_size = static_cast<GLint>(code.size());

    glShaderSource(shader, 1, &c_code, &c_size);
    glCompileShader(shader);

    int success;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        auto log = getGetShaderInfoLogEx(shader);
        throw std::runtime_error(tfm::format("Failed to compile shader: %s", log));
    }

    d.cancel();
    return shader;
}

The defer construct provides a simple yet effective mechanism for managing resources and ensuring exception safety, particularly in contexts where the se of full RAII patterns is impractical or unnecessary.