Classe wrapper C++ per array di oggetti buffer OpenGL: solo i costruttori
Una domanda simile è stata posta qui e sto cercando di fare la stessa cosa. Ma
- Sto provando un approccio diverso derivando da
std::array
, e - Questa è una domanda molto focalizzata solo sui costruttori.
Ecco il mio gl_wrap.h
:
// header guards snipped
#include <array>
#include <algorithm>
#include <cstring>
#include <GL/gl.h>
namespace gl_wrap {
// aliases for style consistency and to mark as parts of gl_wrap
using gl_name = GLuint;
using gl_enum = GLenum;
template<size_t N>
class buffer_objects : public std::array<gl_name, N> {
public:
buffer_objects() noexcept;
~buffer_objects();
buffer_objects(const buffer_objects&) = delete;
buffer_objects operator=(const buffer_objects&) = delete;
buffer_objects(buffer_objects&& from) noexcept;
buffer_objects& operator=(buffer_objects&& from) noexcept;
};
template<size_t N>
buffer_objects<N>::buffer_objects() noexcept
{
glGenBuffers(N, this->data());
}
template<size_t N>
buffer_objects<N>::~buffer_objects()
{
glDeleteBuffers(N, this->data());
}
template<size_t N>
buffer_objects<N>::buffer_objects(buffer_objects<N>&& from) noexcept
: std::array<gl_name, N>(std::move(from))
{
memset(from.data(), 0, N * sizeof(gl_name));
}
template<size_t N>
buffer_objects<N>& buffer_objects<N>::operator=(buffer_objects<N>&& from)
noexcept
{
std::array<gl_name, N>::operator=(std::move(from));
memset(from.data(), 0, N * sizeof(gl_name));
return *this;
}
}
// namespace gl_wrap
Alcune domande specifiche che ho su questo codice/approccio sono, se applicabile,
- Decisione di progettazione sulla gestione degli errori: i costruttori dovrebbero lanciare o dovrei lasciare il controllo degli errori, se lo si desidera, al chiamante?
- Decisione di progettazione: è una buona idea utilizzare i modelli qui? Questo porterà a un problema di gonfiamento del codice se sto usando un sacco di dimensioni diverse
buffer_objects
o è efficiente? Posso valutarlo attraverso la profilazione? - Vale la
memcpy
pena mettere i valori in uno stato di non possesso valido o posso trattare i valori come avanzi di non possesso? - Sto usando correttamente il costruttore di mosse della classe base?
Risposte
Alcune domande specifiche che ho su questo codice/approccio sono, se applicabile,
OK.
Decisione di progettazione sulla gestione degli errori: i costruttori dovrebbero lanciare o dovrei lasciare il controllo degli errori, se lo si desidera, al chiamante?
O l'oggetto è correttamente inizializzato e pronto per l'uso o dovrebbe lanciare. L'inizializzazione in due fasi (costruire quindi controllare) è una cattiva idea in quanto lascia all'utente la possibilità di fare la cosa corretta. Dovresti assicurarti che il tuo oggetto non possa essere abusato, non fare affidamento sull'utente per non abusare di te.
Decisione di progettazione: è una buona idea utilizzare i modelli qui?
Se hai intenzione di usarlo std::array
, non hai davvero scelta.
Questo porterà a un problema di gonfiamento del codice se sto usando molti buffer_objects di dimensioni diverse o è efficiente? Posso valutarlo attraverso la profilazione?
Potenzialmente questo porterà alla compilazione di metodi per diversi tipi. Ma questo dipende dal compilatore e alcuni compilatori moderni possono ottimizzarlo. Ma cosa stai confrontando anche tu?
I memcpy valgono la pena di mettere i rvalue in uno stato di non possesso valido o posso trattare i rvalue come avanzi di non possesso?
Penso che tu debba annullare i src
valori. Altrimenti il distruttore rilascerà i nomi. In alternativa, puoi memorizzare più stati sulla validità dell'oggetto e rendere la chiamata a glDeleteBuffers()
condizionata al fatto che l'oggetto sia in uno stato valido.
Ma penso anche che ci sia un bug qui. Hai appena copiato i valori. Ma non hai rilasciato i valori sul dst
lato prima della copia, quindi hai perso i nomi memorizzati lì.
Ricorda che uno std::array non ha una propria mossa (poiché i dati sono locali all'oggetto non allocati dinamicamente). Sposta gli oggetti sottostanti tra i contenitori.
Sto usando correttamente il costruttore di mosse della classe base?
Sì, sembra legittimo.
Suggerimenti.
Farei in modo che std::array
un membro erediti piuttosto da esso.
Farei:
#ifndef THORSANVIL_GL_WRAPPER_H
#define THORSANVIL_GL_WRAPPER_H
#include <GL/gl.h>
#include <array>
#include <algorithm>
#include <cstring>
namespace ThorsAnvil::GL {
template<size_t N>
class BufferObjects
{
std::array<GLuint, N> buffer;
bool valid;
public:
BufferObjects() noexcept;
~BufferObjects();
// Note: These are non copyable objects.
// Deleting the copy operations.
BufferObjects(BufferObjects const&) = delete;
BufferObjects operator=(BufferObjects const&) = delete;
// Note: The move is as expensive as a copy operation.
// But we are doing a logical move as you
// can not have two objects with the same resource.
BufferObjects(BufferObjects&& from) noexcept;
BufferObjects& operator=(BufferObjects&& from) noexcept;
// Reset an invalid object.
// Note: If object is valid no action.
void reset();
};
template<size_t N>
BufferObjects<N>::BufferObjects() noexcept
: valid(false)
{
reset();
}
template<size_t N>
BufferObjects<N>::~BufferObjects()
{
if (valid) {
glDeleteBuffers(N, buffer.data());
}
}
template<size_t N>
BufferObjects<N>::BufferObjects(BufferObjects<N>&& from) noexcept
{
// Move the resources from the other object.
std::move(std::begin(from.buffer), std::end(from.buffer), std::begin(buffer));
// Maintain the correct valid states
// The rhs is no longer in a valid state and has no resources.
valid = from.valid;
from.valid = false;
}
template<size_t N>
BufferObjects<N>& BufferObjects<N>::operator=(BufferObjects<N>&& from)
noexcept
{
// The standard copy and swap not efficient.
// So we should do a test for self assignment
if (this != &from)
{
// Destroy then re-create this object.
// Call destructor and then placement new to use
// the move copy constructor code to set this value.
~BufferObjects();
new (this) BufferObjects(std::move(from));
}
return *this;
}
template<size_t N>
void BufferObjects::reset()
{
if (!valid) {
glGenBuffers(N, buffer.data());
valid = true;
}
}