Wpisz techniki wymazywania


136

(W przypadku wymazywania typu mam na myśli ukrycie niektórych lub wszystkich informacji o typie dotyczących klasy, trochę jak Boost.Any ).
Chcę poznać techniki wymazywania typu, jednocześnie udostępniając te, które znam. Mam nadzieję, że znajdę jakąś szaloną technikę, o której ktoś pomyślał w swojej najciemniejszej godzinie. :)

Pierwszym i najbardziej oczywistym i powszechnie stosowanym podejściem, jakie znam, są funkcje wirtualne. Po prostu ukryj implementację swojej klasy w hierarchii klas opartej na interfejsie. Wiele bibliotek Boost robi to, na przykład Boost, robi to, aby ukryć twój typ, a Boost.Shared_ptr robi to, aby ukryć mechanikę (de) alokacji.

Następnie jest opcja ze wskaźnikami funkcji do funkcji szablonowych, podczas gdy rzeczywisty obiekt znajduje się we void*wskaźniku, jak Boost. Funkcja robi to, aby ukryć rzeczywisty typ funktora. Przykładowe implementacje można znaleźć na końcu pytania.

Tak więc, odpowiadając na moje aktualne pytanie:
jakie inne techniki wymazywania typu znasz? Jeśli to możliwe, podaj im przykładowy kod, przypadki użycia, swoje doświadczenia z nimi i być może linki do dalszego czytania.

Edytuj
(Ponieważ nie byłem pewien, czy dodać to jako odpowiedź, czy po prostu edytować pytanie, zrobię po prostu bezpieczniejsze).
Inną fajną techniką ukrywania rzeczywistego typu czegoś bez funkcji wirtualnych lub void*majstrowania jest jeden GMan, którego używa tutaj , z odniesieniem do mojego pytania, jak dokładnie to działa.


Przykładowy kod:

#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
Czy przez „wymazywanie typu” masz na myśli „polimorfizm”? Myślę, że "wymazywanie typu" ma dość specyficzne znaczenie, które zwykle kojarzy się np. Z generykami Javy.
Oliver Charlesworth

3
@Oli: Wymazywanie typów można zaimplementować za pomocą polimorfizmu, ale to nie jedyna opcja, mój drugi przykład to pokazuje. :) A z wymazywaniem typów mam na myśli tylko to, że twoja struktura nie zależy na przykład od typu szablonu. Boost.Funkcja nie dba o to, czy podasz jej funktor, wskaźnik funkcji, czy nawet lambdę. To samo z Boost.Shared_Ptr. Możesz określić alokator i funkcję zwalniania alokacji, ale rzeczywisty typ tego shared_ptrnie odzwierciedla, zawsze będzie taki sam, shared_ptr<int>na przykład w przeciwieństwie do standardowego kontenera.
Xeo

2
@Matthieu: Uważam, że drugi przykład również jest bezpieczny. Zawsze wiesz dokładnie, na jakim typie pracujesz. A może coś mi brakuje?
Xeo

2
@Matthieu: Masz rację. Normalnie taka As(e) funkcja nie zostałaby zaimplementowana w ten sposób. Jak powiedziałem, w żadnym wypadku nie jest bezpieczny w użyciu! :)
Xeo

4
@lurscher: Cóż ... nigdy nie korzystałeś z wersji boost lub std któregokolwiek z poniższych? function, shared_ptr, any, Itd.? Wszystkie używają wymazywania typu dla wygody użytkownika słodkich słodyczy.
Xeo

Odpowiedzi:


100

Wszystkie techniki wymazywania typów w C ++ są wykonywane za pomocą wskaźników funkcji (dla zachowania) i void*(dla danych). „Różne” metody różnią się po prostu sposobem dodawania cukru semantycznego. Na przykład funkcje wirtualne są po prostu semantycznym cukrem dla

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

iow: wskaźniki funkcji.

To powiedziawszy, jest jednak jedna technika, którą szczególnie lubię: shared_ptr<void>po prostu dlatego, że odstrasza ludzi, którzy nie wiedzą, że możesz to zrobić: możesz przechowywać dowolne dane w a shared_ptr<void>i nadal mieć właściwy destruktor wywołany w end, ponieważ shared_ptrkonstruktor jest szablonem funkcji i domyślnie użyje typu rzeczywistego obiektu przekazanego do utworzenia usuwania:

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

Oczywiście jest to zwykłe void*wymazywanie typu / wskaźnika funkcji, ale bardzo wygodnie zapakowane.


9
Tak się złożyło, że shared_ptr<void>kilka dni temu musiałem wyjaśnić swoje zachowanie znajomemu przykładową implementacją. :) To naprawdę fajne.
Xeo

Dobra odpowiedź; aby było niesamowicie, szkic tego, jak można statycznie utworzyć fałszywą tabelę dla każdego wymazanego typu, jest bardzo pouczający. Zwróć uwagę, że implementacje fałszywych tabel vtables i wskaźników funkcji zapewniają znane struktury wielkości pamięci (w porównaniu do typów czysto wirtualnych), które można łatwo przechowywać lokalnie i (łatwo) oddzielić od danych, które wirtualizują.
Yakk - Adam Nevraumont

tak więc, jeśli shared_ptr następnie przechowuje Derived *, ale Base * nie zadeklarował destruktora jako wirtualnego, shared_ptr <void> nadal działa zgodnie z przeznaczeniem, ponieważ nigdy nie wiedział nawet o klasie bazowej. Chłodny!
TamaMcGlinn

@Apollys: Tak, ale unique_ptrnie usuwa typu usuwającego, więc jeśli chcesz przypisać a unique_ptr<T>do a unique_ptr<void>, musisz jawnie podać argument deleter, który wie, jak usunąć Tplik za pomocą void*. Jeśli chcesz teraz również przypisać an S, potrzebujesz narzędzia usuwającego, wyraźnie, który wie, jak usunąć a Tdo a, void*a także Sprzez a void*, a biorąc pod uwagę a void*, wie, czy jest to a, Tczy an S. W tym momencie napisałeś kasownik z wymazaniem typu unique_ptr, który działa również dla unique_ptr. Po prostu nie po wyjęciu z pudełka.
Marc Mutz - mmutz

Wydaje mi się, że pytanie, na które odpowiedziałeś, brzmiało: „Jak obejść fakt, że to nie działa unique_ptr?” Przydatne dla niektórych osób, ale nie odpowiadałem na moje pytanie. Myślę, że odpowiedź brzmi, ponieważ wspólne wskazówki przyciągnęły większą uwagę w rozwoju standardowej biblioteki. Co moim zdaniem jest trochę smutne, ponieważ unikalne wskaźniki są prostsze, więc powinno być łatwiej wdrażać podstawowe funkcje i są bardziej wydajne, więc ludzie powinni z nich częściej korzystać. Zamiast tego mamy dokładnie odwrotnie.
Apollys obsługuje Monikę

54

Zasadniczo są to twoje opcje: funkcje wirtualne lub wskaźniki funkcji.

Sposób przechowywania danych i kojarzenia ich z funkcjami może się różnić. Na przykład, możesz przechowywać wskaźnik do bazy i mieć klasę pochodną zawierającą dane i implementacje funkcji wirtualnych, lub możesz przechowywać dane w innym miejscu (np. W osobno przydzielonym buforze), a klasa pochodna zapewni implementacje funkcji wirtualnych, które przyjmują znak, void*który wskazuje na dane. Jeśli przechowujesz dane w oddzielnym buforze, możesz użyć wskaźników funkcji zamiast funkcji wirtualnych.

Przechowywanie wskaźnika do bazy działa dobrze w tym kontekście, nawet jeśli dane są przechowywane oddzielnie, jeśli istnieje wiele operacji, które chcesz zastosować do danych wymazanych przez typ. W przeciwnym razie otrzymasz wiele wskaźników funkcji (po jednym dla każdej funkcji z usuniętym typem) lub funkcje z parametrem określającym operację do wykonania.


1
Czyli innymi słowy przykłady, które podałem w pytaniu? Chociaż, dzięki za napisanie tego w ten sposób, zwłaszcza w odniesieniu do funkcji wirtualnych i wielu operacji na danych wymazanych przez typ.
Xeo

Istnieją co najmniej 2 inne opcje. Tworzę odpowiedź.
John Dibling

25

Chciałbym również rozważyć (podobny do void*) stosowanie „surowego przechowywania”: char buffer[N].

W C ++ 0x masz std::aligned_storage<Size,Align>::typedo tego.

Możesz tam przechowywać wszystko, co chcesz, o ile jest wystarczająco małe i prawidłowo radzisz sobie z wyrównaniem.


4
Cóż, tak, funkcja Boost.Function używa kombinacji tego i drugiego przykładu, który podałem. Jeśli funktor jest wystarczająco mały, przechowuje go wewnętrznie w functor_buffer. Dobrze jednak o tym wiedzieć std::aligned_storage, dzięki! :)
Xeo

Możesz również użyć do tego nowego miejsca docelowego .
rustyx

2
@RustyX: Właściwie to musisz . std::aligned_storage<...>::typejest tylko surowym buforem, który w przeciwieństwie do tego char [sizeof(T)]jest odpowiednio wyrównany. Sama jednak jest bezwładna: nie inicjalizuje swojej pamięci, nie buduje obiektu, niczego. Dlatego, gdy masz bufor tego typu, musisz ręcznie konstruować obiekty w nim (za newpomocą constructmetody umieszczania lub alokatora ) i musisz ręcznie zniszczyć również obiekty w nim (ręcznie wywołując ich destruktor lub używając destroymetody alokatora ).
Matthieu M.

22

Stroustrup, w języku programowania C ++ (wydanie czwarte) §25.3 , stwierdza:

Warianty techniki wykorzystującej pojedynczą reprezentację czasu wykonywania dla wartości wielu typów i polegające na (statycznym) systemie typów w celu zapewnienia, że ​​są one używane tylko zgodnie z zadeklarowanym typem, zostały nazwane wymazaniem typu .

W szczególności nie ma potrzeby korzystania z funkcji wirtualnych ani wskaźników funkcji w celu usunięcia typu, jeśli używamy szablonów. Przykładem tego jest wspomniany już w innych odpowiedziach przypadek prawidłowego wywołania destruktora zgodnie z typem zapisanym w a std::shared_ptr<void>.

Przykład przedstawiony w książce Stroustrupa jest równie przyjemny.

Pomyśl o wdrożeniu template<class T> class Vectorkontenera na wzór std::vector. Kiedy będziesz używał swojego Vectorz wieloma różnymi typami wskaźników, jak to często bywa, kompilator prawdopodobnie wygeneruje inny kod dla każdego typu wskaźnika.

Ten kod uwędzić można zapobiegać poprzez zdefiniowanie specjalizację Vector dla void*wskaźników, a następnie przy użyciu tej specjalizacji do wspólnej realizacji podstawy Vector<T*>dla wszystkich innych typów 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)); }
};

Jak widać, mamy silnie wpisane pojemnik ale Vector<Animal*>, Vector<Dog*>, Vector<Cat*>, ..., będą dzielić ten sam (C ++ i binarny) kod do wykonania, uwzględniając ich rodzaj wskaźnik usunięte tyłu void*.


2
Bez zamiaru bluźnierstwa: wolałbym CRTP od techniki podanej przez Stroustrupa.
davidhigh

@davidhigh Co masz na myśli?
Paolo M

Można uzyskać to samo zachowanie (z mniejszą składnią), używając klasy bazowej CRTP,template<typename Derived> VectorBase<Derived> która jest następnie wyspecjalizowana jako template<typename T> VectorBase<Vector<T*> >. Co więcej, to podejście działa nie tylko w przypadku wskaźników, ale w przypadku każdego typu.
davidhigh

3
Zauważ, że dobre konsolidatory C ++ łączą identyczne metody i funkcje: linker złota lub zwijanie comdat MSVC. Kod jest generowany, ale następnie odrzucany podczas łączenia.
Yakk - Adam Nevraumont

1
@davidhigh Próbuję zrozumieć Twój komentarz i zastanawiam się, czy możesz podać mi link lub nazwę wzorca do wyszukiwania (nie CRTP, ale nazwa techniki, która umożliwia wymazywanie typu bez funkcji wirtualnych lub wskaźników funkcji) . Z poważaniem, - Chris
Chris Chiasson


7

Jak stwierdził Marc, można użyć odlewu std::shared_ptr<void>. Na przykład przechowuj typ we wskaźniku funkcji, rzutuj go i przechowuj w funktorze tylko jednego typu:

#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
    // !
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.