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 对象不再有效的主要内容,如果未能解决你的问题,请参考以下文章

C++ RAII 类中的 OpenGL 对象不再有效

C++基于RAII对锁进行封装

C++ RAII

C++ 类中的指针越来越乱

C++中的RAII和拷贝控制

RAII(Resource Acquisition Is Initialization)简介