C++中的RAII类中的OpenGL对象不再起作用

24

我有一个在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.

发生了什么?

注:这是构建标准答案的尝试。


2
@bartop:“构造函数应该是无代码的”这与现代(甚至较旧的)C++编程的几乎所有思想都相悖。在构造函数中分配资源是智能指针的基石,甚至是C++核心指南的一部分。 - Nicol Bolas
请问,什么?智能指针中没有一个会在其构造函数中分配资源。它们有专门的工厂函数来完成这个任务。通常不建议在构造函数中放置代码,因为错误难以处理,对象可能会处于不可预测的状态。 - bartop
2
@bartop:“智能指针中没有一个在其构造函数中分配资源。”你认为shared_ptr的共享状态来自哪里?该共享状态必须动态分配,以便它可以被其他shared_ptr实例共享,并且它需要能够比资源存在更长时间,以便weak_ptr可以工作。shared_ptr在其构造函数中为共享状态分配内存。这忽略了标准库中的每个容器,如果您传递要存储的数据,则所有容器都在其构造函数中分配。或者文件流,在其构造函数中打开文件等。 - Nicol Bolas
1
@bartop:所以,尽管你可能个人认为“构造函数应该是无代码的”,但实际上C++并不是这样做的。从Boost到Qt再到Poco,几乎每个C++库都有执行实际工作的对象构造函数。这是RAII的基础。 “错误很难处理,对象可能处于不可预测的状态”这就是异常存在的原因。 - Nicol Bolas
1
关于什么是三法则的相关内容。 - Jarod42
显示剩余2条评论
2个回答

34
所有这些操作都会复制 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将被销毁。因此,它将调用glDeleteBuffers来删除这个OpenGL对象。但是,向量中的对象仍然具有其自己的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.
};

各种其他技术可以用于创建OpenGL对象的移动语义RAII包装器。


我做了类似的事情,但是添加了一个布尔值“has_resources”来携带,而不是检查buff_是否为0。可以安全地假设没有任何东西会被分配为0作为ID吗? - Barnack
2
@Barnack:零不是缓冲对象的有效名称。对于大多数OpenGL对象也是如此。即使对于其中允许使用零的对象,它也不代表您可以成功删除的对象(使用0调用glDelete *将导致什么都不发生)。 - Nicol Bolas
@Nicol Bolas 我按照您的示例进行了操作(感谢),但似乎不起作用。当我将其声明为T(T&& other)而不是T(const T&)时,它似乎没有用新构造函数替换旧构造函数,这很令人沮丧,因为我们删除了旧构造函数并没有使用新构造函数。有什么建议吗? - איתן טורפז
@איתןטורפז:我不知道你的意思。我不知道T是什么,也不知道你为什么要做任何“替代”我在这里所做的事情。你不是“替换”任何东西;你删除了复制操作并实现了移动操作。就像我在这里做的一样。 - Nicol Bolas
在你的例子中,TBufferObject 或其他类类型(抱歉没有说明)。我的意思是编译器没有将移动操作识别为“新”的复制操作。因此,当它尝试调用复制构造函数时,它就被删除了。 - איתן טורפז
显示剩余5条评论

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数组)放在头文件中,这样就不会生成链接器错误。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接