24

I have an OpenGL object in a C++ class. Since I'm employing RAII, I want to have the destructor delete it. So my class looks something like:

class BufferObject
{
private:
  GLuint buff_;

public:
  BufferObject()
  {
    glGenBuffers(1, &buff_);
  }

  ~BufferObject()
  {
    glDeleteBuffers(1, &buff_);
  }

//Other members.
};

This seems like it works. But any time I do any of the following, I start getting various OpenGL errors when I use it:

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.

What's going on?

Note: this is an attempt to build a canonical answer to these questions.

vallentin
  • 23,478
  • 6
  • 59
  • 81
Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • 2
    @bartop: "*Constructor should be code-free*" That goes against pretty much every idea of modern (or even older) C++ programming. Allocating resources in constructors is a cornerstone of smart pointers, and it's even part of the C++ Core Guidelines. – Nicol Bolas Jul 02 '19 at 13:31
  • excuse me, what? None of smart pointers allocates resources in its constructor. They have special factory functions for this purpose. It is generally bad idea to put code in constructor because errors are hard to handle and object may be left in unpredictable state – bartop Jul 02 '19 at 14:22
  • 2
    @bartop: "*None of smart pointers allocates resources in its constructor.*" Where do you think the shared state for a `shared_ptr` comes from? That shared state has to be dynamically allocated so that it can be shared by other `shared_ptr` instances, and it needs to be able to outlive the resource so that `weak_ptr` works. `shared_ptr` allocates memory for the shared state in its constructor. And that ignores literally every container in the standard library, all of which allocate in their constructors if you pass them data to store. Or file streams which open files in their constructors. Etc. – Nicol Bolas Jul 02 '19 at 14:40
  • 1
    @bartop: So while you may personally believe that "constructor should be code-free", that's just not how C++ is done in practice. From Boost to Qt to Poco, pretty much every C++ library has object constructors that do actual work. It's the basis of RAII. "*errors are hard to handle and object may be left in unpredictable state*" That's what exceptions are for. – Nicol Bolas Jul 02 '19 at 14:41
  • And `make_shared` is suggested method of creating `shared_ptr` also because it does not involve exceptions and allocation in constructor. I disagree this is basis of RAII. Even Cpp core guidelines say throwing from constructor is bad idea so this ain't just my idea: https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rc-throw. It is reasonable rule of thumb – bartop Jul 02 '19 at 17:47
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/195871/discussion-between-nicol-bolas-and-bartop). – Nicol Bolas Jul 02 '19 at 17:48
  • 1
    Related to [what-is-the-rule-of-three](https://stackoverflow.com/questions/4172722/what-is-the-rule-of-three) – Jarod42 Jul 05 '19 at 18:05

2 Answers2

33

All of those operations copy the C++ object. Since your class did not define a copy constructor, you get the compiler-generated copy constructor. This simply copies all of the members of the object.

Consider the first example:

vector<BufferObject> bufVec;
{
  BufferObject some_buffer;
  //Initialize some_buffer;
  bufVec.push_back(some_buffer);
}
bufVec.back(); //buffer doesn't work.

When you call push_back, it copies some_buffer into a BufferObject in the vector. So, right before we exit that scope, there are two BufferObject objects.

But what OpenGL buffer object do they store? Well, they store the same one. After all, to C++, we just copied an integer. So both C++ objects store the same integer value.

When we exit that scope, some_buffer will be destroyed. Therefore, it will call glDeleteBuffers on this OpenGL object. But the object in the vector will still have its own copy of that OpenGL object name. Which has been destroyed.

So you can't use it anymore; hence the errors.

The same thing happens with your InitBuffer function. buff will be destroyed after it is copied into the return value, which makes the returned object worthless.

This is all due to a violation of the so-called "Rule of 3/5" in C++. You created a destructor without creating copy/move constructors/assignment operators. That's bad.

To solve this, your OpenGL object wrappers should be move-only types. You should delete the copy constructor and copy assignment operator, and provide move equivalents that set the moved-from object to object 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.
};

There are various other techniques for making move-only RAII wrappers for OpenGL objects.

vallentin
  • 23,478
  • 6
  • 59
  • 81
Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • I did a similar thing but adding a boolean "has_resources" to carry around instead of checking if buff_ is 0. Is it safe to assume nothing will be assigned 0 as id? – Barnack May 20 '20 at 12:58
  • 2
    @Barnack: Zero is not a valid name for buffer objects. Or for most OpenGL objects. And even for the ones where it is valid, it doesn't represent an object you can *delete* (successfully; calling `glDelete*` with a 0 will result in nothing happening). – Nicol Bolas May 20 '20 at 13:22
  • @NicolBolas Thanks for asking this question, and self-answering. I have a question though. You're calling `Release()` in the move assignment operator, but shouldn't the method simply move the id without releasing the buffer at that point as it's transferring ownership? – John H Nov 14 '20 at 23:30
  • @JohnH: The `this` object in move assignment is the object being assigned to. That object may still have ownership of a buffer. In order to claim ownership of a new one, it must "release" ownership of any buffer it already owns. A common alternative is to swap the two objects, leaving the formerly owned buffer in the other object which will itself be eventually destroyed. – Nicol Bolas Nov 14 '20 at 23:55
  • @NicolBolas Ah, I see what you're saying! I misread it. Thanks for clarifying. – John H Nov 14 '20 at 23:59
  • @Nicol Bolas I followed your example (thank you btw) but it doesn't seem to work, when I decalre it as ```T(T&& other)``` instead of ```T(const T&)``` it doesn't seem to replace to new constructor with the last one, it's just sad because we deleted the old one and doesn't use the new one. any suggestions? – איתן טורפז May 23 '21 at 14:09
  • @איתןטורפז: I don't know what you mean. I don't know what `T` is, nor do I know why you're trying to do anything "instead of" what I did here. You don't "replace" anything; you delete the copy operations and implement the move ones. As I did here. – Nicol Bolas May 23 '21 at 14:12
  • `T` is `BufferObject` in your example, or any other class type sorry for no saying it. I meant that the compiler didn't recognize the move operation as the 'new' copy operation. so when it tried to call the copy constructor it was just deleted – איתן טורפז May 23 '21 at 14:17
  • @איתןטורפז: "*I meant that the compiler didn't recognize the move operation as the 'new' copy operation.*" Move does not "replace" the copy; it isn't a "'new' copy operation". Moving only happens when you *ask for it* (or in certain, specific circumstances), such as `return`ing a local variable. You should look up how move works in C++. – Nicol Bolas May 23 '21 at 14:19
  • Sorry didn't understood it before. I did however found a solution, I don't delete the copy constructor, I just tell it to call the move constructor whenever it's called. So I did 'replace' the copy constructor with the move one after all... Thank you – איתן טורפז May 23 '21 at 14:23
2

All of your operations copy the buffer object. But as your class don't have copy constructor, it's just a shallow copy. As your destructor deletes the buffer without further checking, the buffer is deleted with the original object. Nicol Bolas suggested to define a move constructor and delete copy constructor and copy assignment operator, I would describe a different way so both buffers will be usable after a copy.

You can keep a track of how many objects use a single easily with an std::map array. Consider the following example code which is an extension of your code:

#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
};

The code is pretty self-explaining. When the buffer object is initialized, a new buffer is created. Then the constructor creates a new element in reference_count array with the buffer as key and sets its value as 1. Whenever the object is copied, the count increases. An when the object is destructed, the count decreases. Then destructor checks if the count is 0 or less, which mean buffer is no longer needed, so the buffer is deleted.

I recommend to not put the implementation (or least the reference_count array) in a header file so linker errors are not generated.

Akib Azmain Turja
  • 1,142
  • 7
  • 27