Digitare tecniche di cancellazione


136

(Con la cancellazione del tipo, intendo nascondere alcune o tutte le informazioni sul tipo relative a una classe, un po 'come Boost . Qualunque .)
Voglio ottenere delle tecniche di cancellazione del tipo, condividendo anche quelle che conosco. La mia speranza è di trovare una tecnica folle a cui qualcuno abbia pensato nella sua ora più buia. :)

Il primo e più ovvio e comunemente adottato approccio, che conosco, sono funzioni virtuali. Nascondi l'implementazione della tua classe all'interno di una gerarchia di classi basata su interfaccia. Molte librerie di Boost fanno questo, per esempio Boost. Qualcuno lo fa per nascondere il tuo tipo e Boost.Shared_ptr lo fa per nascondere il meccanico (de) allocazione.

Quindi c'è l'opzione con i puntatori di funzione alle funzioni di modello, mentre si tiene l'oggetto reale in un void*puntatore, come Boost.Function fa per nascondere il tipo reale del funzione. Implementazioni di esempio sono disponibili alla fine della domanda.

Quindi, per la mia vera domanda:
quali altri tipi di tecniche di cancellazione conosci? Fornisci loro, se possibile, un codice di esempio, casi d'uso, la tua esperienza con essi e forse collegamenti per ulteriori letture.

Modifica
(Dal momento che non ero sicuro di aggiungere questo come risposta, o semplicemente modificare la domanda, farò solo la più sicura.)
Un'altra bella tecnica per nascondere il tipo reale di qualcosa senza funzioni virtuali o void*giocherellare, è la un GMan impiega qui , in relazione alla mia domanda su come funziona esattamente.


Codice di esempio:

#include <iostream>
#include <string>

// NOTE: The class name indicates the underlying type erasure technique

// this behaves like the Boost.Any type w.r.t. implementation details
class Any_Virtual{
        struct holder_base{
                virtual ~holder_base(){}
                virtual holder_base* clone() const = 0;
        };

        template<class T>
        struct holder : holder_base{
                holder()
                        : held_()
                {}

                holder(T const& t)
                        : held_(t)
                {}

                virtual ~holder(){
                }

                virtual holder_base* clone() const {
                        return new holder<T>(*this);
                }

                T held_;
        };

public:
        Any_Virtual()
                : storage_(0)
        {}

        Any_Virtual(Any_Virtual const& other)
                : storage_(other.storage_->clone())
        {}

        template<class T>
        Any_Virtual(T const& t)
                : storage_(new holder<T>(t))
        {}

        ~Any_Virtual(){
                Clear();
        }

        Any_Virtual& operator=(Any_Virtual const& other){
                Clear();
                storage_ = other.storage_->clone();
                return *this;
        }

        template<class T>
        Any_Virtual& operator=(T const& t){
                Clear();
                storage_ = new holder<T>(t);
                return *this;
        }

        void Clear(){
                if(storage_)
                        delete storage_;
        }

        template<class T>
        T& As(){
                return static_cast<holder<T>*>(storage_)->held_;
        }

private:
        holder_base* storage_;
};

// the following demonstrates the use of void pointers 
// and function pointers to templated operate functions
// to safely hide the type

enum Operation{
        CopyTag,
        DeleteTag
};

template<class T>
void Operate(void*const& in, void*& out, Operation op){
        switch(op){
        case CopyTag:
                out = new T(*static_cast<T*>(in));
                return;
        case DeleteTag:
                delete static_cast<T*>(out);
        }
}

class Any_VoidPtr{
public:
        Any_VoidPtr()
                : object_(0)
                , operate_(0)
        {}

        Any_VoidPtr(Any_VoidPtr const& other)
                : object_(0)
                , operate_(other.operate_)
        {
                if(other.object_)
                        operate_(other.object_, object_, CopyTag);
        }

        template<class T>
        Any_VoidPtr(T const& t)
                : object_(new T(t))
                , operate_(&Operate<T>)
        {}

        ~Any_VoidPtr(){
                Clear();
        }

        Any_VoidPtr& operator=(Any_VoidPtr const& other){
                Clear();
                operate_ = other.operate_;
                operate_(other.object_, object_, CopyTag);
                return *this;
        }

        template<class T>
        Any_VoidPtr& operator=(T const& t){
                Clear();
                object_ = new T(t);
                operate_ = &Operate<T>;
                return *this;
        }

        void Clear(){
                if(object_)
                        operate_(0,object_,DeleteTag);
                object_ = 0;
        }

        template<class T>
        T& As(){
                return *static_cast<T*>(object_);
        }

private:
        typedef void (*OperateFunc)(void*const&,void*&,Operation);

        void* object_;
        OperateFunc operate_;
};

int main(){
        Any_Virtual a = 6;
        std::cout << a.As<int>() << std::endl;

        a = std::string("oh hi!");
        std::cout << a.As<std::string>() << std::endl;

        Any_Virtual av2 = a;

        Any_VoidPtr a2 = 42;
        std::cout << a2.As<int>() << std::endl;

        Any_VoidPtr a3 = a.As<std::string>();
        a2 = a3;
        a2.As<std::string>() += " - again!";
        std::cout << "a2: " << a2.As<std::string>() << std::endl;
        std::cout << "a3: " << a3.As<std::string>() << std::endl;

        a3 = a;
        a3.As<Any_Virtual>().As<std::string>() += " - and yet again!!";
        std::cout << "a: " << a.As<std::string>() << std::endl;
        std::cout << "a3->a: " << a3.As<Any_Virtual>().As<std::string>() << std::endl;

        std::cin.get();
}

1
Con "tipo di cancellazione", ti riferisci davvero al "polimorfismo"? Penso che la "cancellazione del tipo" abbia un significato un po 'specifico, che di solito è associato ad esempio ai generici Java.
Oliver Charlesworth,

3
@Oli: la cancellazione del tipo può essere implementata con il polimorfismo, ma questa non è l'unica opzione, il mio secondo esempio mostra che. :) E con la cancellazione del tipo intendo solo che la tua struttura non dipende, ad esempio, da un tipo di modello. Boost.Function non importa se gli dai un funzione, un puntatore a funzione o persino un lambda. Lo stesso con Boost.Shared_Ptr. È possibile specificare un allocatore e una funzione di deallocazione, ma il tipo effettivo di shared_ptrnon riflette questo, sarà sempre lo stesso, shared_ptr<int>ad esempio, a differenza del contenitore standard.
Xeo

2
@Matthieu: considero anche il secondo esempio un tipo sicuro. Conosci sempre il tipo esatto su cui stai operando. Oppure mi sfugge qualcosa?
Xeo

2
@Matthieu: hai ragione. Normalmente una Aso più funzioni di questo tipo non sarebbero implementate in questo modo. Come ho detto, non è sicuro da usare! :)
Xeo

4
@lurscher: Beh ... non hai mai usato le versioni boost o std di nessuno dei seguenti? function, shared_ptr, any, Ecc? Tutti utilizzano la cancellazione del tipo per una dolce comodità dell'utente.
Xeo,

Risposte:


100

Tutte le tecniche di cancellazione dei tipi in C ++ vengono eseguite con puntatori a funzioni (per comportamento) e void*(per dati). I metodi "diversi" differiscono semplicemente nel modo in cui aggiungono zucchero semantico. Le funzioni virtuali, ad esempio, sono solo zucchero semantico per

struct Class {
    struct vtable {
        void (*dtor)(Class*);
        void (*func)(Class*,double);
    } * vtbl
};

iow: puntatori a funzione.

Detto questo, c'è una tecnica che mi piace particolarmente, però: è shared_ptr<void>, semplicemente perché fa esplodere le menti delle persone che non sanno che puoi farlo: puoi archiviare tutti i dati in un shared_ptr<void>e avere ancora il distruttore corretto chiamato al fine, perché il shared_ptrcostruttore è un modello di funzione e utilizzerà il tipo di oggetto effettivo passato per la creazione del deleter per impostazione predefinita:

{
    const shared_ptr<void> sp( new A );
} // calls A::~A() here

Naturalmente, questa è solo la solita void*cancellazione / puntatore a funzione, ma molto convenientemente impacchettata.


9
Per coincidenza, ho dovuto spiegare il comportamento di shared_ptr<void>un mio amico con un esempio di implementazione solo qualche giorno fa. :) È davvero bello.
Xeo,

Buona risposta; per renderlo sorprendente, uno schizzo di come una falsa tabella può essere creata staticamente per ogni tipo cancellato è molto istruttivo. Si noti che le implementazioni fake-vtables e del puntatore a funzione forniscono strutture note della dimensione della memoria (rispetto ai tipi puramente virtuali) che possono essere facilmente archiviate localmente e (facilmente) divorziate dai dati che stanno virtualizzando.
Yakk - Adam Nevraumont,

quindi, se shared_ptr quindi memorizza un derivato *, ma Base * non ha dichiarato il distruttore come virtuale, shared_ptr <void> funziona ancora come previsto, poiché non ha mai saputo nemmeno di una classe base per cominciare. Freddo!
TamaMcGlinn,

@Apollys: Lo fa, ma unique_ptrnon cancella il tipo del deleter, quindi se si desidera assegnare un unique_ptr<T>a a unique_ptr<void>, è necessario fornire un argomento deleter, in modo esplicito, che sappia come eliminare il Tthrough a void*. Se ora si vuole assegnare un S, troppo, allora avete bisogno di un deleter, in modo esplicito, che sa come eliminare una Tattraverso una void*e anche Sattraverso un void*, e , data una void*, sa che si tratti di una To di un S. A quel punto, hai scritto un deleter cancellato dal tipo per unique_ptr, e poi funziona anche per unique_ptr. Solo non fuori dalla scatola.
Marc Mutz - mmutz,

Sento che la domanda a cui hai risposto era "Come posso risolvere il fatto che non funziona unique_ptr?" Utile per alcune persone, ma non ha risposto alla mia domanda. Immagino che la risposta sia, perché i puntatori condivisi hanno ricevuto maggiore attenzione nello sviluppo della libreria standard. Il che penso sia un po 'triste perché i puntatori unici sono più semplici, quindi dovrebbe essere più semplice implementare le funzionalità di base e sono più efficienti, quindi le persone dovrebbero usarle di più. Invece abbiamo l'esatto contrario.
Apollys sostiene Monica il

54

Fondamentalmente, queste sono le tue opzioni: funzioni virtuali o puntatori a funzioni.

Il modo in cui archiviare i dati e associarli alle funzioni può variare. Ad esempio, è possibile memorizzare un puntatore alla base e fare in modo che la classe derivata contenga i dati e le implementazioni della funzione virtuale, oppure si possano archiviare i dati altrove (ad es. In un buffer allocato separatamente) e fare in modo che la classe derivata fornisca le implementazioni di funzioni virtuali, che prendono un punto void*che punta ai dati. Se si memorizzano i dati in un buffer separato, è possibile utilizzare i puntatori a funzione anziché le funzioni virtuali.

La memorizzazione di un puntatore alla base funziona bene in questo contesto, anche se i dati sono memorizzati separatamente, se ci sono più operazioni che si desidera applicare ai dati cancellati dal tipo. Altrimenti si finisce con più puntatori a funzione (uno per ciascuna delle funzioni cancellate dal tipo) o con un parametro che specifica l'operazione da eseguire.


1
Quindi, in altre parole, gli esempi che ho dato alla domanda? Tuttavia, grazie per averlo scritto in questo modo, specialmente per le funzioni virtuali e le operazioni multiple sui dati cancellati dal tipo.
Xeo,

Ci sono almeno altre 2 opzioni. Sto componendo una risposta.
John Dibling,

25

Vorrei anche prendere in considerazione (simile a void*) l'uso di "storage raw": char buffer[N].

In C ++ 0x hai std::aligned_storage<Size,Align>::typeper questo.

Puoi conservare tutto quello che vuoi lì, purché sia ​​abbastanza piccolo e gestisci correttamente l'allineamento.


4
Bene sì, Boost.Function utilizza effettivamente una combinazione di questo e del secondo esempio che ho dato. Se il functor è abbastanza piccolo, lo memorizza internamente all'interno del functor_buffer. Buono a sapersi std::aligned_storageperò, grazie! :)
Xeo

Puoi anche utilizzare il posizionamento nuovo per questo.
Rustyx,

2
@RustyX: In realtà, si hanno a. std::aligned_storage<...>::typeè solo un buffer grezzo che, a differenza char [sizeof(T)], è opportunamente allineato. Di per sé, tuttavia, è inerte: non inizializza la sua memoria, non costruisce un oggetto, niente. Pertanto, una volta che si dispone di un buffer di questo tipo, è necessario costruire manualmente oggetti al suo interno (con posizionamento newo con un constructmetodo allocatore ) e anche distruggere manualmente gli oggetti al suo interno (invocando manualmente il loro distruttore o usando un destroymetodo allocatore ).
Matthieu M.,

22

Stroustrup, in Il linguaggio di programmazione C ++ (4a edizione) §25.3 , afferma:

Varianti della tecnica di utilizzo di una singola rappresentazione del tempo di esecuzione per valori di un numero di tipi e basandosi sul sistema di tipo (statico) per garantire che vengano utilizzati solo in base al loro tipo dichiarato è stata chiamata cancellazione del tipo .

In particolare, non è necessario l' uso di funzioni virtuali o puntatori a funzione per eseguire la cancellazione del tipo se si utilizzano modelli. Il caso, già menzionato in altre risposte, della chiamata corretta del distruttore in base al tipo memorizzato in a ne std::shared_ptr<void>è un esempio.

L'esempio fornito nel libro di Stroustrup è altrettanto divertente.

Pensa all'implementazione template<class T> class Vector, un contenitore sulla falsariga di std::vector. Quando utilizzerai il tuo Vectorcon molti tipi di puntatori diversi, come spesso accade, il compilatore genererà presumibilmente un codice diverso per ogni tipo di puntatore.

Questo eccesso di codice può essere evitato definendo una specializzazione di Vector per i void*puntatori e quindi utilizzando questa specializzazione come implementazione di base comune Vector<T*>per tutti gli altri tipi T:

template<typename T>
class Vector<T*> : private Vector<void*>{
// all the dirty work is done once in the base class only 
public:
    // ...
    // static type system ensures that a reference of right type is returned
    T*& operator[](size_t i) { return reinterpret_cast<T*&>(Vector<void*>::operator[](i)); }
};

Come potete vedere, abbiamo un contenitore fortemente tipizzato, ma Vector<Animal*>, Vector<Dog*>, Vector<Cat*>, ..., condividerà la stessa (C ++ e binario) il codice per l'attuazione, che hanno la loro tipo di puntatore cancellata dietro void*.


2
Senza significato essere blasfemo: preferirei CRTP alla tecnica data da Stroustrup.
davidhigh,

@davidhigh Che vuoi dire?
Paolo M,

Si può ottenere lo stesso comportamento (con una sintassi meno acuta) usando una classe base CRTPtemplate<typename Derived> VectorBase<Derived> che è quindi specializzata come template<typename T> VectorBase<Vector<T*> >. Inoltre, questo approccio non funziona solo per i puntatori, ma per qualsiasi tipo.
davidhigh,

3
Nota che buoni linker C ++ uniscono metodi e funzioni identici: il gold linker o MSVC comdat pieghevole. Il codice viene generato, ma poi scartato durante il collegamento.
Yakk - Adam Nevraumont,

1
@davidhigh Sto cercando di capire il tuo commento e mi chiedo se puoi darmi un link o un nome di un modello per cui cercare (non il CRTP, ma il nome di una tecnica che consente la cancellazione del tipo senza funzioni virtuali o puntatori a funzioni) . Rispettosamente, - Chris
Chris Chiasson,


7

Come affermato da Marc, si può usare il cast std::shared_ptr<void>. Ad esempio, memorizza il tipo in un puntatore a funzione, esegui il cast e memorizza in un funzione di un solo tipo:

#include <iostream>
#include <memory>
#include <functional>

using voidFun = void(*)(std::shared_ptr<void>);

template<typename T>
void fun(std::shared_ptr<T> t)
{
    std::cout << *t << std::endl;
}

int main()
{
    std::function<void(std::shared_ptr<void>)> call;

    call = reinterpret_cast<voidFun>(fun<std::string>);
    call(std::make_shared<std::string>("Hi there!"));

    call = reinterpret_cast<voidFun>(fun<int>);
    call(std::make_shared<int>(33));

    call = reinterpret_cast<voidFun>(fun<char>);
    call(std::make_shared<int>(33));


    // Output:,
    // Hi there!
    // 33
    // !
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.