【发布时间】:2021-03-04 08:01:25
【问题描述】:
我非常热衷于使用 SDL、OpenGL 和 C++ 开发游戏,并且正在寻找方法来优化游戏在 GLSL 着色器之间切换的方式,以处理不同类型的许多不同对象。这更像是一个 C++ 问题,而不是 OpenGL 问题。但是,我仍然想提供尽可能多的上下文,因为我觉得需要一些理由来说明为什么我需要的提议的 Shader 类需要按原样创建/删除。
前四部分是我的理由、旅程和尝试,但我的问题可能仅通过最后一部分来回答,我特意把它写成一个 tldr。
Shader 类的必要性:
在游戏过程中创建游戏对象时,我已经看到许多在同一函数中创建、编译和删除 OpenGL 着色器的在线实现。事实证明,这在我的游戏的特定部分效率低下且速度太慢。因此,我需要一个系统,它可以在加载期间创建和编译着色器,然后在游戏期间间歇性地在它们之间使用/交换,然后再被删除。
这导致创建了一个管理 OpenGL 着色器的类 (Shader)。该类的每个实例都应管理一个唯一的 OpenGL 着色器,并包含围绕着色器类型的一些复杂行为,从哪里加载,在哪里使用,它采用的统一变量等。
话虽如此,这个类最重要的作用是存储从glCreateShader()返回的GLuint变量id,并用这个id管理所有与OpenGL着色器相关的OpenGL调用。我知道考虑到 OpenGL 的全局性质,这实际上是徒劳的(因为程序中的任何地方都可以在技术上调用 glDeleteShader() 与匹配的 id 并破坏类),但是为了有意将所有 OpenGL 调用封装到非常具体的该系统将在整个代码库中大大降低代码复杂性。
问题从哪里开始...
管理此GLuint id 的最“自动”方式是在对象的构造上调用glCreateShader(),在对象的销毁上调用glDeleteShader()。这保证(在 OpenGL 限制内)OpenGL 着色器将存在于 C++ Shader 对象的整个生命周期中,并且无需调用某些 void createShader() 和 deleteShader() 函数。
这一切都很好,但是当考虑复制此对象会发生什么时,很快就会出现问题。如果这个对象的副本被破坏了怎么办?这意味着glDeleteShader() 将被调用并有效地破坏着色器对象的所有副本。
在着色器向量中意外调用std::vector::push_back() 等简单错误怎么办?各种std::vector 方法可以调用其类型的构造函数/复制构造函数/析构函数,这可能会导致与上述相同的问题。
好吧……我们创建一些void createShader() 和deleteShader() 方法怎么样,即使它很乱?不幸的是,这只是推迟了上述问题,因为修改 OpenGL 着色器的 任何 调用将再次取消同步/彻底破坏具有相同 id 的着色器类的所有副本。在此示例中,我将 OpenGL 调用限制为 glCreateShader() 和 glDeleteShader() 以保持简单,但是我应该注意,类中还有许多其他 OpenGL 调用可以创建各种实例/静态变量来跟踪实例副本过于复杂,无法证明这样做是合理的。
在进入下面的类设计之前,我想说的最后一点是,对于像原始 C++、OpenGL 和 SDL 游戏这样大的项目,如果我犯的任何潜在的 OpenGL 错误会生成编译器错误而不是图形化错误,我会更喜欢更难追踪的问题。这可以反映在下面的类设计中。
Shader类的第一个版本:
基于以上原因,我选择了:
- 制作构造函数
private。 - 提供一个公共
static create函数,该函数返回一个指向新 Shader 对象的指针来代替构造函数。 - 制作复制构造函数
private。 - 制作
operator=private(尽管这可能不是必需的)。 - 将析构函数设为私有。
- 在构造函数中调用
glCreateShader(),在析构函数中调用glDeleteShader(),以使OpenGL着色器在该对象的生命周期内存在。 - 当
create函数调用new关键字(并返回指向它的指针)时,外部调用Shader::create()的地方必须手动调用delete(稍后会详细介绍)。
据我了解,前两个要点使用工厂模式,如果尝试创建类的非指针类型,则会生成编译器错误。第三、第四和第五个要点然后防止对象被复制。然后第七个要点确保 OpenGL 着色器将在 C++ 着色器对象的相同生命周期内存在。
智能指针和主要问题:
我唯一不喜欢上述内容的是new/delete 电话。考虑到类试图实现的封装,它们还使对象的析构函数中的glDeleteShader() 调用感觉不合适。鉴于此,我选择:
- 将
create函数更改为返回Shader类型的std::unique_ptr,而不是Shader指针。
create 函数看起来像这样:
std::unique_ptr<Shader> Shader::create() {
return std::make_unique<Shader>();
}
但随后出现了一个新问题……不幸的是,std::make_unique 要求 构造函数 是 public,这会干扰上一节中描述的必要性。幸运的是,我找到了解决方案,将其更改为:
std::unique_ptr<Shader> Shader::create() {
return std::unique_ptr<Shader>(new Shader());
}
但是...现在std::unique_ptr 要求 析构函数 是公开的!这……更好但不幸的是,这意味着可以在类外部手动调用析构函数,这反过来意味着可以从类外部调用glDeleteShader() 函数。
Shader* p = Shader::create();
p->~Shader(); // Even though it would be hard to do this intentionally, I don't want to be able to do this.
delete p;
最后一节课:
为简单起见,我删除了大部分实例变量、函数/构造函数参数和其他属性,但最终提议的类(大部分)如下所示:
class GLSLShader {
public:
~GLSLShader() { // OpenGL delete calls for id }; // want to make this private.
static std::unique_ptr<GLSLShader> create() { return std::unique_ptr<GLSLShader>(new GLSLShader()); };
private:
GLSLShader() { // OpenGL create calls for id };
GLSLShader(const GLSLShader& glslShader);
GLSLShader& operator=(const GLSLShader&);
GLuint id;
};
我对这个类中的所有内容都很满意,除了析构函数是公开的。我已经对这个设计进行了测试,性能提升非常明显。尽管我无法想象我会不小心手动调用 Shader 对象上的析构函数,但我不喜欢它公开。我也觉得我可能会不小心漏掉一些东西,比如第二部分中的std::vector::push_back 考虑。
我找到了两种可能的解决方案来解决这个问题。我想要一些关于这些或其他解决方案的建议。
-
使
std::unique_ptr或std::make_unique成为Shader类的friend。我一直在阅读诸如this one 之类的线程,但是这是为了使构造函数可以访问,而不是析构函数。我也不太明白将std::unique_ptr或std::make_unique设为friend(该线程的最佳答案+ cmets)所需的缺点/额外考虑因素? -
根本不使用智能指针。有没有办法让我的
static create()函数返回一个原始指针(使用new关键字),当Shader超出范围并调用析构函数时,它会在类 / 内自动删除?
非常感谢您的宝贵时间。
【问题讨论】:
-
我没有阅读你所有的问题,但我认为足以理解这个想法。在我看来,您真正需要的只是一个返回
std::shared_ptrs 的工厂方法?这不是解决你所有的问题吗? -
此外,试图“保护用户”免于手动调用析构函数的想法至少可以说是可疑的。如果他们想打破你的班级,他们总是能够做到的。没有人会不小心手动调用析构函数。
-
保护墨菲,而不是马基雅维利。这个练习过度考虑了一些(几乎绝对肯定)不会成为问题的设计。
-
“手动调用析构函数”?谁这样做?如果有人恶意或无能,他们可以编辑您的头文件并删除
private:。或者加个好友。或者做他们想做的任何事情。 @StoryTeller-UnslanderMonica 说得很好:“保护墨菲,而不是马基雅维利。”不管怎样,谁会针对这个接口编程呢? 你不会犯手动调用析构函数的错误... -
@Jaymaican 我理解你的想法。但是,您有一个智能指针来管理对象的生命周期这一事实消除了所有这些担忧。智能指针永远不会复制底层对象(除非由用户明确完成,而且这在此处也是不可能的,因为您有私有副本 ctor 和分配)。如果工厂方法是创建实例的唯一方法,那么您的基础已经涵盖。
标签: c++ destructor smart-pointers friend-class