Classe wrapper C++ para array de objetos de buffer OpenGL — apenas os construtores
Uma pergunta semelhante foi feita aqui e estou tentando fazer a mesma coisa. Mas
- Estou tentando uma abordagem diferente derivando de
std::array
, e - Esta é uma questão muito focada apenas sobre construtores.
Aqui está o meu 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
Algumas perguntas específicas que tenho sobre este código/abordagem são, se aplicável,
- Decisão de design de tratamento de erros: os construtores devem lançar ou devo deixar a verificação de erros, se desejado, para o chamador?
- Decisão de design: é uma boa ideia usar modelos aqui? Isso levará a um inchaço de código problemático se eu estiver usando muitos tamanhos diferentes
buffer_objects
ou é eficiente? Posso avaliar isso por meio de perfis? - Vale a pena colocar os rvalues
memcpy
em um estado não proprietário válido ou posso tratar os rvalues como sobras não proprietárias? - Estou usando corretamente o construtor move da classe base?
Respostas
Algumas perguntas específicas que tenho sobre este código/abordagem são, se aplicável,
OK.
Decisão de design de tratamento de erros: os construtores devem lançar ou devo deixar a verificação de erros, se desejado, para o chamador?
Ou o objeto foi inicializado corretamente e está pronto para uso ou deve ser lançado. A inicialização em dois estágios (construir e verificar) é uma má ideia, pois deixa em aberto para o usuário fazer a coisa certa. Você deve certificar-se de que seu objeto não pode ser abusado e não confiar no usuário para não abusar de você.
Decisão de design: é uma boa ideia usar modelos aqui?
Se você vai usar std::array
, não tem escolha.
Isso levará a um inchaço de código problemático se eu estiver usando muitos buffer_objects de tamanhos diferentes ou é eficiente? Posso avaliar isso por meio de perfis?
Potencialmente, isso levará a que métodos sejam compilados para tipos diferentes. Mas isso depende do compilador e algum compilador moderno pode otimizar isso. Mas o que você está comparando também?
Os memcpys valem a pena colocar os rvalues em um estado não proprietário válido ou posso tratar os rvalues como sobras não proprietárias?
Acho que você precisa anular os src
valores. Caso contrário, o destruidor irá liberar os nomes. Como alternativa, você pode armazenar mais informações sobre se o objeto é válido e fazer a chamada glDeleteBuffers()
condicional se o objeto estiver em um estado válido.
Mas também acho que há um bug aqui. Você acabou de copiar os valores. Mas você não liberou os valores do dst
lado antes da cópia, então perdeu os nomes armazenados lá.
Lembre-se de que um std::array não possui seu próprio movimento (já que os dados são locais para o objeto não alocados dinamicamente). Ele move os objetos subjacentes entre contêineres.
Estou usando corretamente o construtor move da classe base?
Sim, parece legítimo.
Sugestões.
Eu faria com que std::array
um membro herdasse dele.
Eu faria:
#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;
}
}