OpenGL Shader类中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
相关产品推荐
相关产品推荐

