Implementazione del conteggio dei riferimenti da zero o utilizzando shared_ptr per la risorsa?

4

In un'applicazione OpenGL che sto scrivendo, voglio avere una semplice classe shader per racchiudere l'handle dello shader OpenGL. In definitiva, voglio che questa classe di shader si comporti in modo molto simile a un shared_ptr in c ++ (cioè, mantieni un conteggio dei riferimenti e libera la risorsa quando non ci sono riferimenti). Mentre è relativamente banale implementare questo conteggio dei riferimenti da zero, mi chiedevo se fosse considerata una scelta di design migliore per usare invece std::shared_ptr con un deleter personalizzato per liberare la risorsa.

La principale fonte del mio dubbio è il fatto che possa essere considerato non convenzionale a causa del fatto che (per quanto ne so), la creazione di un handle di programma shader OpenGL in realtà non coinvolge la memoria heap (che è ciò che shared_ptr si prende cura di) ma ritengo ancora che possa essere applicato qui perché voglio che questa risorsa sia gestita in modo molto simile a come la memoria dell'heap verrà gestita. Lo scopo di questa domanda è fondamentalmente cercare l'opinione degli altri sul fatto che questo sia effettivamente non convenzionale, perché non lo so.

Inoltre, anche se ho usato gli shader come esempio, la stessa domanda si applica anche alle trame e ai buffer in OpenGL poiché anch'essi devono essere allocati e liberati.

    
posta Anonymous Person 21.06.2018 - 00:05
fonte

2 risposte

8

unique_ptr<T, D> è in realtà progettato appositamente per essere in grado di lavorare con tipi di handle più arbitrari. Ho spiegato completamente il nome del modello perché D è la chiave qui. Normalmente unique_ptr<T, D>::get() restituisce un T* . Questa è l'impostazione predefinita, ma può essere sostituita da D : il tipo deleter.

Se il tipo deleter ha un alias pointer ( D::pointer è una sintassi legale), quindi unique_ptr<T, D>::get() restituirà quel tipo. Questo ti permette di avere qualcosa come unique_ptr<GLuint, gl::program_deleter> , dove gl::program_deleter::pointer è di tipo int .

Porto tutto questo perché shared_ptr non può farlo . unique_ptr<T, D> viene eliminato perché il deleter è in realtà parte del tipo unique_ptr stesso. Al contrario, mentre i costruttori di shared_ptr possono prendere un deleter, l'unica cosa che la funzione deleter può fare è cancellare la memoria.

Quindi shared_ptr<GLuint>::get() restituirà sempre GLuint* . Ciò significa che, se si desidera utilizzare shared_ptr come una sorta di tipo di handle condiviso, quel tipo deve essere assegnato in modo dinamico in qualche modo. È possibile che non si stia utilizzando l'heap globale, ma non è possibile archiviare e restituire l'intero. shared_ptr<T> contiene sempre T* .

Quindi, non importa cosa, dovrai gestire GLuint* s se vuoi usare il meccanismo di conteggio dei riferimenti di shared_ptr . Sì, il deleter può essere utilizzato per chiamare glDeleteProgram o qualsiasi cosa tu desideri, ma shared_ptr<GLuint> continuerà a memorizzare GLuint* .

creating an OpenGL shader program handle does not actually involve heap memory

OK, dimentichiamo per un momento che, creando un oggetto OpenGL, il driver ha quasi certamente accumulato un po 'di memoria. Diamo un'occhiata a ciò che devi fare.

Creando un shared_ptr a cui appartiene un certo spazio di archiviazione, verrà assegnato un qualcosa . Vale a dire, il blocco condiviso che gestisce il conteggio dei riferimenti di shared_ptr . Non c'è modo di aggirare questo. Pertanto, se desideri utilizzare l'infrastruttura di conteggio di riferimento di shared_ptr , dovrai allocare da qualche parte.

Quindi il modo più idiomatico per farlo è quello di ammucchiare solo allocare un GLuint e usare un deleter speciale che distrugge l'oggetto OpenGL e rilascia il numero intero. Non è carino ed è un po 'dispendioso, ma non è affatto terribile. E se utilizzi make_shared , puoi rendere le cose abbastanza compatte in termini di allocazioni.

Ora puoi evitare questa allocazione con imbrogli . Puoi fare ciò:

GLuint program = glCreateProgram();
shared_ptr<GLuint> sp(reinterpret_cast<GLuint*>(program), ProgramDeleter);

Quindi, qui prendiamo un numero intero e lo gettiamo su un valore puntatore, per essere memorizzato all'interno di shared_ptr . Quando hai bisogno di usarlo, devi invertire il cast per recuperare il valore intero.

Ma giudica il seguente codice:

glProgramUniform1i(reinterpret_cast<GLuint>(sp.get()), val);

Sembra che tu voglia fare qualcosa di frequente? Sembra che tu voglia leggere di frequente? Sembra qualcosa che qualcun altro capirà facilmente cosa sta succedendo?

Non solo, non puoi mai usare *sp per ottenere il valore, poiché il valore del puntatore è il valore in questione.

Oh, e il blocco di controllo del conteggio dei riferimenti viene ancora assegnato all'heap, quindi non è come se impedissi di allocare memoria o qualcosa del genere.

Questo non è un C ++ idiomatico.

    
risposta data 21.06.2018 - 03:10
fonte
0

Quando stavo sperimentando questo, ho scritto un set specifico di classi handle e ho finito per non utilizzare i puntatori intelligenti standard perché li trovavo un po 'troppo pesanti.

Sorgente completa qui:

link

Ho usato il servizio / maneggia l'idioma per separare le preoccupazioni sul comportamento delle maniglie (condiviso o unico) e sulla funzionalità.

Un estratto qui:

/// Resource Service base implementation
/// When a resource_object manages its resources (e.g. copy, move, construction, destruction etc) it will
/// defer to its service object. The service object will be a concrete class derived from this class.
/// The reason for this is that the service can then handle the details around creating and deleting
/// GL handle objects and managing their lifetimes.
/// @tparam Derived is the concrete service class derived from this class
/// @tparam NativeType is the type used to store the underlying GL handle or collection of handles
template<typename Derived, typename NativeType = GLuint>
struct basic_resource_service;

template<class Derived>
struct basic_resource_service<Derived, GLuint> : notstd::stateless_service<Derived>
{
    using native_handle_type = GLuint;
    using implementation_type = native_handle_type;

    /// Determine whether the implementation is empty,i.e. does not represent a GL handle.
    bool empty(implementation_type const &impl) const noexcept
    {
        return not impl;
    }

    void invalidate(implementation_type& impl) const noexcept
    {
        impl = 0;
    }
};

/// A specialisation of basic_resource_service which handles a vector of GL handles
template<class Derived, class DataType>
struct basic_resource_service<Derived, std::vector<DataType>>
{
    using implementation_type = std::vector<DataType>;

    bool empty(implementation_type const &impl) const
    {
        return not impl.empty();
    }

    void invalidate(implementation_type& impl) const
    {
        impl.clear();
    }
};

struct shader_service : basic_resource_service<shader_service, GLuint>
{
    /// Construct a shader identity of a given type
    /// @param type is a gl shader type enum
    /// @return the gl id of a new shader
    ///
    static auto construct(shader_type type) -> implementation_type;

    /// Destroy a gl shader object if not zero
    /// @param impl is a reference to a shader id
    /// @pre impl contains either a valid shader object id or 0
    /// @post impl shall contain 0
    ///
    static auto destroy(implementation_type &impl) -> void;


    static std::size_t log_length(implementation_type const &impl);

    static std::string log(implementation_type const &impl);

    static std::size_t source_length(implementation_type const &impl);

    static std::string source(implementation_type const &impl);
};

struct shader_compilation_failed : std::runtime_error
{
    using std::runtime_error::runtime_error;
};

/// The representation of some kind of shader
struct shader : notstd::unique_handle<shader_service>
{
    shader(shader_type type)
        : notstd::unique_handle<shader_service>(std::make_tuple(type))
    {
    }

    template<class...Sources>
    shader(shader_type type, Sources &&...sources)
        : shader(type)
    {
        constexpr auto count = sizeof...(sources);

        const GLchar *sz_sources[] =
            {
                detail::to_gl_char(sources)...
            };

        const GLint lengths[] = {
            detail::get_gl_string_length(sources)...
        };

        glShaderSource(native_handle(), count, sz_sources, lengths);
        glCompileShader(native_handle());
        check_errors("shader::shader");
        if( not compiled())
        {
            throw shader_compilation_failed(log());
        }
    }

    /// Return the source code for this shader object, if it has any
    auto source() const -> std::string;

    /// Return the shader type
    auto type() const -> shader_type;

    /// Check whether the shader has compiled
    auto compiled() const -> bool;

    /// Return the log text associated with this shader
    auto log() const -> std::string;
};

struct fragment_shader : shader
{
    template
        <
            class String,
            std::enable_if_t
                <
                    not std::is_base_of<shader, std::decay_t<String>>::value
                > * = nullptr
        >
    fragment_shader(String &&str)
        : shader(shader_type::fragment, std::forward<String>(str))
    {

    }
};
    
risposta data 26.06.2018 - 10:38
fonte

Leggi altre domande sui tag