Classe wrapper C++ para array de objetos de buffer OpenGL — apenas os construtores

Aug 18 2020

Uma pergunta semelhante foi feita aqui e estou tentando fazer a mesma coisa. Mas

  1. Estou tentando uma abordagem diferente derivando de std::array, e
  2. 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,

  1. 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?
  2. 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_objectsou é eficiente? Posso avaliar isso por meio de perfis?
  3. 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?
  4. Estou usando corretamente o construtor move da classe base?

Respostas

5 MartinYork Aug 18 2020 at 04:43

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 srcvalores. 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 dstlado 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::arrayum 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;
    }
}