You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

OpenGL Shader类中shared_ptr自定义删除器的最优实现方案问询

OpenGL着色器包装器的shared_ptr自定义删除器最佳实现方案

问题背景

我正在编写一个OpenGL着色器的轻量包装器,每个着色器创建后会从OpenGL获取ID,Shader类保存该ID并提供相关实用功能,且需要支持拷贝与移动。

最初的Shader类实现存在严重问题:

class Shader
{
    public:
        Shader(ShaderType type, std::string shaderSource) { /*glCreateShader, glCompileShader*/ }
        ~Shader() { glDeleteShader(m_rendererId) }

    private:
        unsigned int m_rendererId = 0;
}

当拷贝Shader对象后销毁其中一个实例时,其他对象的m_rendererId会指向已被OpenGL删除的着色器对象,导致悬空引用问题。

方案对比与最佳选择

针对用std::shared_ptr<unsigned int>管理着色器ID的两种方案,为shared_ptr提供自定义删除器是最佳选择,原因如下:

  • 符合RAII核心思想:将资源释放逻辑完全绑定到shared_ptr,无需手动跟踪引用计数,代码简洁且不易出错。
  • 避免竞态条件:依赖use_count() == 1判断是否释放资源的做法在多线程环境下不安全——use_count()的返回值在检查完成后可能立即被其他线程修改,导致glDeleteShader被错误调用或遗漏调用。
  • 更好的封装性:删除器与资源生命周期绑定,无需在类的析构函数中编写额外判断逻辑,降低代码维护成本。

实现示例

基础实现(存储unsigned int指针)

class Shader
{
public:
    Shader(ShaderType type, std::string shaderSource) 
    {
        unsigned int shaderId = glCreateShader(static_cast<GLenum>(type));
        // 执行着色器编译逻辑(略)
        // 用自定义删除器初始化shared_ptr
        m_rendererId = std::shared_ptr<unsigned int>(
            new unsigned int(shaderId),
            [](unsigned int* id) {
                if (*id != 0) {
                    glDeleteShader(*id);
                    delete id;
                }
            }
        );
    }

    // 拷贝构造、赋值运算符默认即可,shared_ptr会自动处理引用计数
    Shader(const Shader&) = default;
    Shader& operator=(const Shader&) = default;

    // 移动构造、赋值运算符同样默认支持
    Shader(Shader&&) = default;
    Shader& operator=(Shader&&) = default;

    // 提供获取着色器ID的接口
    unsigned int getId() const { return *m_rendererId; }

private:
    std::shared_ptr<unsigned int> m_rendererId;
};

优化实现(避免堆分配)

如果想避免额外的堆分配(存储unsigned int的内存),可以用std::shared_ptr<void>直接存储着色器ID:

class Shader
{
public:
    Shader(ShaderType type, std::string shaderSource) 
    {
        unsigned int shaderId = glCreateShader(static_cast<GLenum>(type));
        // 执行着色器编译逻辑(略)
        m_rendererId = std::shared_ptr<void>(
            reinterpret_cast<void*>(shaderId),
            [](void* ptr) {
                unsigned int id = reinterpret_cast<unsigned int>(ptr);
                if (id != 0) {
                    glDeleteShader(id);
                }
            }
        );
    }

    unsigned int getId() const {
        return reinterpret_cast<unsigned int>(m_rendererId.get());
    }

    // 默认构造、赋值运算符(略)

private:
    std::shared_ptr<void> m_rendererId;
};

为什么方案2不可取

方案2中在析构函数里判断use_count() == 1再调用glDeleteShader的做法存在明显缺陷:

  • 多线程场景下use_count()的返回值不具备原子性,检查和删除操作之间可能有其他线程修改引用计数,导致重复释放或资源泄漏。
  • 手动干预资源释放逻辑,违背了RAII的设计初衷,增加了代码出错的风险。

内容的提问来源于stack exchange,提问作者Eugene

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 08:20:34