C++ RAII 类中的 OpenGL 对象不再有效
Posted
技术标签:
【中文标题】C++ RAII 类中的 OpenGL 对象不再有效【英文标题】:OpenGL object in C++ RAII class no longer works 【发布时间】:2017-10-19 22:04:53 【问题描述】:我在 C++ 类中有一个 OpenGL 对象。由于我使用的是 RAII,我想让析构函数删除它。所以我的课看起来像:
class BufferObject
private:
GLuint buff_;
public:
BufferObject()
glGenBuffers(1, &buff_);
~BufferObject()
glDeleteBuffers(1, &buff_);
//Other members.
;
这似乎有效。但是每当我执行以下任何操作时,我都会在使用它时遇到各种 OpenGL 错误:
vector<BufferObject> bufVec;
BufferObject some_buffer;
//Initialize some_buffer;
bufVec.push_back(some_buffer);
bufVec.back(); //buffer doesn't work.
BufferObject InitBuffer()
BufferObject buff;
//Do stuff with `buff`
return buff;
auto buff = InitBuffer(); //Returned buffer doesn't work.
发生了什么事?
注意:这是对这些问题建立规范答案的尝试。
【问题讨论】:
@bartop: "构造函数应该是无代码的" 这与现代(甚至更早)C++ 编程的几乎所有想法背道而驰。在构造函数中分配资源是智能指针的基石,它甚至是 C++ 核心指南的一部分。 对不起,什么?没有一个智能指针在其构造函数中分配资源。为此,它们具有特殊的工厂功能。将代码放在构造函数中通常是个坏主意,因为错误很难处理,并且对象可能会处于不可预测的状态 @bartop: "没有一个智能指针在其构造函数中分配资源。" 你认为shared_ptr
的共享状态来自哪里?该共享状态必须动态分配,以便它可以被其他shared_ptr
实例共享,并且它需要能够比资源寿命更长,以便weak_ptr
工作。 shared_ptr
在其构造函数中为共享状态分配内存。这实际上忽略了标准库中的每个容器,如果您将数据传递给它们,所有这些容器都会在它们的构造函数中分配。或在其构造函数中打开文件的文件流。等等。
@bartop:因此,虽然您可能个人认为“构造函数应该是无代码的”,但这并不是 C++ 在实践中的做法。从 Boost 到 Qt 再到 Poco,几乎每个 C++ 库都具有执行实际工作的对象构造函数。这是RAII的基础。 “错误难以处理,对象可能处于不可预知的状态”这就是例外。
与what-is-the-rule-of-three相关
【参考方案1】:
所有这些操作都会复制 C++ 对象。由于您的类没有定义复制构造函数,因此您将获得编译器生成的复制构造函数。这只是复制对象的所有成员。
考虑第一个例子:
vector<BufferObject> bufVec;
BufferObject some_buffer;
//Initialize some_buffer;
bufVec.push_back(some_buffer);
bufVec.back(); //buffer doesn't work.
当您调用push_back
时,它会将some_buffer
复制到vector
中的BufferObject
。因此,在我们退出该范围之前,有两个 BufferObject
对象。
但它们存储的是什么 OpenGL 缓冲区对象?嗯,他们存储相同的。毕竟,对于 C++,我们只是复制了一个整数。所以两个 C++ 对象都存储相同的整数值。
当我们退出该范围时,some_buffer
将被销毁。因此,它将在这个 OpenGL 对象上调用glDeleteBuffers
。但是向量中的对象仍然有它自己的那个 OpenGL 对象名称的副本。哪个被销毁了。
所以你不能再使用它了;因此出现错误。
InitBuffer
函数也会发生同样的事情。 buff
复制到返回值后会被销毁,使得返回的对象一文不值。
这都是因为违反了 C++ 中所谓的“3/5 规则”。您创建了一个析构函数,但没有创建复制/移动构造函数/赋值运算符。这很糟糕。
要解决这个问题,您的 OpenGL 对象包装器应该是仅移动类型。您应该删除复制构造函数和复制赋值运算符,并提供将移动对象设置为对象 0 的移动等效项:
class BufferObject
private:
GLuint buff_;
public:
BufferObject()
glGenBuffers(1, &buff_);
BufferObject(const BufferObject &) = delete;
BufferObject &operator=(const BufferObject &) = delete;
BufferObject(BufferObject &&other) : buff_(other.buff_)
other.buff_ = 0;
BufferObject &operator=(BufferObject &&other)
//ALWAYS check for self-assignment
if(this != &other)
Release();
buff_ = other.buff_;
other.buff_ = 0;
return *this;
~BufferObject() Release();
void Release();
if(buff_)
glDeleteBuffers(1, &buff_);
//Other members.
;
various other techniques 用于为 OpenGL 对象制作仅移动的 RAII 包装器。
【讨论】:
我做了类似的事情,但添加了一个布尔值“has_resources”来携带而不是检查 buff_ 是否为 0。假设什么都不会被分配 0 作为 id 是否安全? @Barnack:零不是缓冲区对象的有效名称。或者对于大多数 OpenGL 对象。即使对于有效的对象,它也不代表您可以删除的对象(成功;使用 0 调用glDelete*
将不会导致任何事情发生)。
@NicolBolas 感谢您提出这个问题并自行回答。但我有个问题。您在移动赋值运算符中调用 Release()
,但该方法不应该只是移动 id 而不释放缓冲区,因为它正在转移所有权?
@JohnH:移动分配中的this
对象是被分配的对象。该对象可能仍然拥有缓冲区的所有权。为了获得新缓冲区的所有权,它必须“释放”它已经拥有的任何缓冲区的所有权。一种常见的替代方法是交换两个对象,将以前拥有的缓冲区留在另一个对象中,该对象最终将被销毁。
@NicolBolas 啊,我明白你在说什么!我误读了。感谢您的澄清。【参考方案2】:
您的所有操作都会复制缓冲区对象。但是由于你的类没有复制构造函数,它只是一个浅拷贝。由于您的析构函数在没有进一步检查的情况下删除了缓冲区,因此缓冲区与原始对象一起被删除。 Nicol Bolas 建议定义一个移动构造函数和删除复制构造函数和复制赋值运算符,我将描述一种不同的方式,以便在复制后两个缓冲区都可以使用。
您可以使用std::map
数组轻松跟踪使用单个对象的数量。考虑以下示例代码,它是您的代码的扩展:
#include <map>
std::map<unsigned int, unsigned int> reference_count;
class BufferObject
private:
GLuint buff_;
public:
BufferObject()
glGenBuffers(1, &buff_);
reference_count[buff_] = 1; // Set reference count to it's initial value 1
~BufferObject()
reference_count[buff_]--; // Decrease reference count
if (reference_count[buff_] <= 0) // If reference count is zero, the buffer is no longer needed
glDeleteBuffers(1, &buff_);
BufferObject(const BufferObject& other) : buff_(other.buff_)
reference_count[buff_]++; // Increase reference count
BufferObject operator = (const BufferObject& other)
if (buff_ != other.buff_) // Check if both buffer is same
buff_ = other.buff_;
reference_count[buff_]++; // Increase reference count
// Other stuffs
;
代码非常不言自明。初始化缓冲区对象时,会创建一个新缓冲区。然后构造函数在reference_count
数组中创建一个以缓冲区为键的新元素,并将其值设置为1。每当复制对象时,计数都会增加。当对象被破坏时,计数减少。然后析构函数检查计数是否为0或更少,这意味着不再需要缓冲区,因此删除缓冲区。
我建议不要将实现(或至少 reference_count
数组)放在头文件中,以免生成链接器错误。
【讨论】:
以上是关于C++ RAII 类中的 OpenGL 对象不再有效的主要内容,如果未能解决你的问题,请参考以下文章