Classe wrapper C++ pour un tableau d'objets tampon OpenGL - uniquement les constructeurs

Aug 18 2020

Une question similaire a été posée ici , et j'essaie de faire la même chose. Mais

  1. J'essaie une approche différente en dérivant de std::array, et
  2. Il s'agit d'une question très ciblée sur les constructeurs uniquement.

Voici mon 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

Certaines questions spécifiques que j'ai au sujet de ce code/approche sont, le cas échéant,

  1. Décision de conception de la gestion des erreurs : les constructeurs doivent-ils lancer ou dois-je laisser la vérification des erreurs, si nécessaire, à l'appelant ?
  2. Décision de conception : est-ce une bonne idée d'utiliser des modèles ici ? Cela conduira-t-il à un gonflement problématique du code si j'utilise beaucoup de tailles différentes buffer_objects, ou est-ce efficace? Puis-je évaluer cela via le profilage ?
  3. Les memcpys valent-ils la peine de mettre les rvalues ​​dans un état non propriétaire valide, ou puis-je traiter les rvalues ​​comme des restes non propriétaires?
  4. Est-ce que j'utilise correctement le constructeur de déplacement de la classe de base ?

Réponses

5 MartinYork Aug 18 2020 at 04:43

Certaines questions spécifiques que j'ai au sujet de ce code/approche sont, le cas échéant,

D'ACCORD.

Décision de conception de la gestion des erreurs : les constructeurs doivent-ils lancer ou dois-je laisser la vérification des erreurs, si nécessaire, à l'appelant ?

Soit l'objet est correctement initialisé et prêt à l'emploi, soit il doit lancer. L'initialisation en deux étapes (construction puis vérification) est une mauvaise idée car elle laisse la possibilité à l'utilisateur de faire ce qu'il faut. Vous devez vous assurer que votre objet ne peut pas être abusé et ne pas compter sur l'utilisateur pour ne pas vous abuser.

Décision de conception : est-ce une bonne idée d'utiliser des modèles ici ?

Si vous allez utiliser std::array, vous n'avez pas vraiment le choix.

Cela conduira-t-il à un gonflement problématique du code si j'utilise beaucoup d'objets tampons de tailles différentes, ou est-ce efficace ? Puis-je évaluer cela via le profilage ?

Cela conduira potentiellement à compiler des méthodes pour différents types. Mais cela dépend du compilateur et certains compilateurs modernes peuvent optimiser cela. Mais qu'est-ce que tu compares aussi ?

Les memcpys valent-ils la peine de mettre les rvalues ​​dans un état non propriétaire valide, ou puis-je traiter les rvalues ​​comme des restes non propriétaires?

Je pense qu'il faut annuler les srcvaleurs. Sinon, le destructeur va libérer les noms. Alternativement, vous pouvez stocker plus d'état pour savoir si l'objet est valide et rendre l'appel glDeleteBuffers()conditionnel à ce que l'objet soit dans un état valide.

Mais je pense aussi qu'il y a un bug ici. Vous venez de recopier les valeurs. Mais vous n'avez pas publié les valeurs sur le dstcôté avant la copie, vous avez donc perdu les noms qui y sont stockés.

N'oubliez pas qu'un std :: array n'a pas son propre mouvement (car les données sont locales à l'objet non alloué dynamiquement). Il déplace les objets sous-jacents entre les conteneurs.

Est-ce que j'utilise correctement le constructeur de déplacement de la classe de base ?

Oui, il semble ligit.

Suggestions.

J'en ferais std::arrayplutôt hériter un membre.

Je ferais:

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