OpenGL-Objekt in der C++-RAII-Klasse funktioniert nicht mehr

Lesezeit: 6 Minuten

OpenGL Objekt in der C RAII Klasse funktioniert nicht mehr
Nicol Bola

Ich habe ein OpenGL-Objekt in einer C++-Klasse. Da ich RAII verwende, möchte ich, dass der Destruktor es löscht. Meine Klasse sieht also in etwa so aus:

class BufferObject
{
private:
  GLuint buff_;

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

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

//Other members.
};

Das scheint zu funktionieren. Aber jedes Mal, wenn ich eines der folgenden Dinge mache, erhalte ich verschiedene OpenGL-Fehler, wenn ich es verwende:

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.

Was ist los?

Hinweis: Dies ist ein Versuch, eine kanonische Antwort auf diese Fragen zu erstellen.

  • @bartop: “Der Konstruktor sollte codefrei sein” Das widerspricht so ziemlich jeder Idee der modernen (oder sogar älteren) C++-Programmierung. Das Zuweisen von Ressourcen in Konstruktoren ist ein Eckpfeiler von Smart Pointern und sogar Teil der C++ Core Guidelines.

    – Nicol Bolas

    2. Juli 2019 um 13:31 Uhr


  • Entschuldigung Was? Keiner der intelligenten Zeiger weist Ressourcen in seinem Konstruktor zu. Dafür haben sie spezielle Werksfunktionen. Es ist im Allgemeinen keine gute Idee, Code im Konstruktor abzulegen, da Fehler schwer zu handhaben sind und das Objekt möglicherweise in einem unvorhersehbaren Zustand verbleibt

    – Bartop

    2. Juli 2019 um 14:22 Uhr


  • @bartop: “Keiner der intelligenten Zeiger weist Ressourcen in seinem Konstruktor zu.„Wo denkst du den geteilten Zustand für a shared_ptr kommt von? Dieser gemeinsam genutzte Zustand muss dynamisch zugewiesen werden, damit er von anderen gemeinsam genutzt werden kann shared_ptr Instanzen, und es muss in der Lage sein, die Ressource damit zu überleben weak_ptr funktioniert. shared_ptr weist Speicher für den gemeinsam genutzten Zustand in seinem Konstruktor zu. Und das ignoriert buchstäblich jeden Container in der Standardbibliothek, die alle in ihren Konstruktoren zuweisen, wenn Sie ihnen Daten zum Speichern übergeben. Oder Dateistreams, die Dateien in ihren Konstruktoren öffnen. Usw.

    – Nicol Bolas

    2. Juli 2019 um 14:40 Uhr

  • @bartop: Während Sie vielleicht persönlich glauben, dass “Konstruktoren codefrei sein sollten”, wird C++ in der Praxis einfach nicht so gemacht. Von Boost über Qt bis hin zu Poco hat so ziemlich jede C++-Bibliothek Objektkonstruktoren, die die eigentliche Arbeit erledigen. Es ist die Grundlage von RAII. “Fehler sind schwer zu handhaben und das Objekt kann in einem unvorhersehbaren Zustand verbleiben„Dafür sind Ausnahmen da.

    – Nicol Bolas

    2. Juli 2019 um 14:41 Uhr


  • Bezogen auf Was-ist-die-Drei-Regel

    – Jarod42

    5. Juli 2019 um 18:05 Uhr

OpenGL Objekt in der C RAII Klasse funktioniert nicht mehr
Nicol Bola

Alle diese Operationen kopieren das C++-Objekt. Da Ihre Klasse keinen Kopierkonstruktor definiert hat, erhalten Sie den vom Compiler generierten Kopierkonstruktor. Dadurch werden einfach alle Mitglieder des Objekts kopiert.

Betrachten Sie das erste Beispiel:

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

Wenn du anrufst push_backes kopiert some_buffer in ein BufferObject in dem vector. Also, kurz bevor wir diesen Bereich verlassen, gibt es zwei BufferObject Objekte.

Aber welches OpenGL-Pufferobjekt speichern sie? Nun, sie speichern Das gleiche. Schließlich haben wir in C++ nur eine Ganzzahl kopiert. Beide C++-Objekte speichern also denselben ganzzahligen Wert.

Wenn wir diesen Bereich verlassen, some_buffer wird zerstört werden. Deshalb wird es anrufen glDeleteBuffers auf diesem OpenGL-Objekt. Aber das Objekt im Vektor hat immer noch eine eigene Kopie dieses OpenGL-Objektnamens. Was hat zerstört worden.

Sie können es also nicht mehr verwenden; daher die fehler.

Das gleiche passiert mit Ihrem InitBuffer Funktion. buff wird nach dem Kopieren in den Rückgabewert zerstört, wodurch das zurückgegebene Objekt wertlos wird.

Dies alles ist auf einen Verstoß gegen die sogenannte „3/5-Regel“ in C++ zurückzuführen. Sie haben einen Destruktor erstellt, ohne Konstruktoren zum Kopieren/Verschieben/Zuweisungsoperatoren zu erstellen. Das ist schlecht.

Um dies zu lösen, sollten Ihre OpenGL-Objekt-Wrapper reine Verschiebetypen sein. Du solltest löschen den Kopierkonstruktor und den Kopierzuweisungsoperator und stellen Verschiebungsäquivalente bereit, die das verschobene Objekt auf Objekt 0 setzen:

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

Es gibt verschiedene andere Techniken, um Move-Only-RAII-Wrapper für OpenGL-Objekte zu erstellen.

  • Ich habe etwas Ähnliches getan, aber einen booleschen Wert “has_resources” hinzugefügt, um ihn herumzutragen, anstatt zu prüfen, ob buff_ 0 ist. Kann man davon ausgehen, dass nichts als ID 0 zugewiesen wird?

    – Barack

    20. Mai 2020 um 12:58 Uhr

  • @Barnack: Null ist kein gültiger Name für Pufferobjekte. Oder für die meisten OpenGL-Objekte. Und selbst für diejenigen, bei denen es gültig ist, stellt es kein Objekt dar, das Sie können löschen (erfolgreich; Aufruf glDelete* mit einer 0 führt dazu, dass nichts passiert).

    – Nicol Bolas

    20. Mai 2020 um 13:22 Uhr

  • @NicolBolas Danke, dass du diese Frage gestellt und selbst beantwortet hast. Ich habe aber eine Frage. Du rufst an Release() im Move-Zuweisungsoperator, aber sollte die Methode nicht einfach die ID verschieben, ohne den Puffer an diesem Punkt freizugeben, wenn der Besitz übertragen wird?

    – Johannes H

    14. November 2020 um 23:30 Uhr


  • @JohnH: Die this Objekt in Bewegungszuweisung ist das Objekt, dem zugewiesen wird. Dieses Objekt kann immer noch Eigentümer eines Puffers sein. Um das Eigentum an einem neuen zu beanspruchen, muss es das Eigentum an jedem Puffer, den es bereits besitzt, “freigeben”. Eine übliche Alternative besteht darin, die beiden Objekte auszutauschen, wobei der zuvor besessene Puffer in dem anderen Objekt verbleibt, das schließlich selbst zerstört wird.

    – Nicol Bolas

    14. November 2020 um 23:55 Uhr


  • @NicolBolas Ah, ich verstehe, was du sagst! Ich habe es falsch gelesen. Danke fürs klarstellen.

    – Johannes H

    14. November 2020 um 23:59 Uhr

Alle Ihre Operationen kopieren das Pufferobjekt. Aber da Ihre Klasse keinen Kopierkonstruktor hat, ist es nur eine flache Kopie. Da Ihr Destruktor den Puffer ohne weitere Überprüfung löscht, wird der Puffer mit dem ursprünglichen Objekt gelöscht. Nicol Bolas schlug vor, einen Bewegungskonstruktor zu definieren und einen Kopierkonstruktor und einen Kopierzuweisungsoperator zu löschen. Ich würde einen anderen Weg beschreiben, damit beide Puffer nach einer Kopie verwendet werden können.

Mit einem können Sie ganz einfach nachverfolgen, wie viele Objekte Sie verwenden std::map Reihe. Betrachten Sie den folgenden Beispielcode, der eine Erweiterung Ihres Codes ist:

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

Der Code ist ziemlich selbsterklärend. Wenn das Pufferobjekt initialisiert wird, wird ein neuer Puffer erstellt. Dann erstellt der Konstruktor ein neues Element in reference_count Array mit dem Puffer als Schlüssel und setzt seinen Wert auf 1. Immer wenn das Objekt kopiert wird, erhöht sich der Zählerstand. Und wenn das Objekt zerstört wird, verringert sich die Anzahl. Dann prüft der Destruktor, ob die Anzahl 0 oder kleiner ist, was bedeutet, dass der Puffer nicht mehr benötigt wird, also wird der Puffer gelöscht.

Ich empfehle, die Implementierung (oder zumindest die reference_count array) in einer Header-Datei, damit keine Linker-Fehler generiert werden.

986050cookie-checkOpenGL-Objekt in der C++-RAII-Klasse funktioniert nicht mehr

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy