
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.

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_back
es 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.
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.
9860500cookie-checkOpenGL-Objekt in der C++-RAII-Klasse funktioniert nicht mehryes
@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 kannshared_ptr
Instanzen, und es muss in der Lage sein, die Ressource damit zu überlebenweak_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