(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();
}
źródło
shared_ptr
nie odzwierciedla, zawsze będzie taki sam,shared_ptr<int>
na przykład w przeciwieństwie do standardowego kontenera.As
(e) funkcja nie zostałaby zaimplementowana w ten sposób. Jak powiedziałem, w żadnym wypadku nie jest bezpieczny w użyciu! :)function
,shared_ptr
,any
, Itd.? Wszystkie używają wymazywania typu dla wygody użytkownika słodkich słodyczy.Odpowiedzi:
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 dlaiow: 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 ashared_ptr<void>
i nadal mieć właściwy destruktor wywołany w end, ponieważshared_ptr
konstruktor jest szablonem funkcji i domyślnie użyje typu rzeczywistego obiektu przekazanego do utworzenia usuwania:Oczywiście jest to zwykłe
void*
wymazywanie typu / wskaźnika funkcji, ale bardzo wygodnie zapakowane.źródło
shared_ptr<void>
kilka dni temu musiałem wyjaśnić swoje zachowanie znajomemu przykładową implementacją. :) To naprawdę fajne.unique_ptr
nie usuwa typu usuwającego, więc jeśli chcesz przypisać aunique_ptr<T>
do aunique_ptr<void>
, musisz jawnie podać argument deleter, który wie, jak usunąćT
plik za pomocąvoid*
. Jeśli chcesz teraz również przypisać anS
, potrzebujesz narzędzia usuwającego, wyraźnie, który wie, jak usunąć aT
do a,void*
a takżeS
przez avoid*
, a biorąc pod uwagę avoid*
, wie, czy jest to a,T
czy anS
. W tym momencie napisałeś kasownik z wymazaniem typuunique_ptr
, który działa również dlaunique_ptr
. Po prostu nie po wyjęciu z pudełka.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.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.
źródło
Chciałbym również rozważyć (podobny do
void*
) stosowanie „surowego przechowywania”:char buffer[N]
.W C ++ 0x masz
std::aligned_storage<Size,Align>::type
do tego.Możesz tam przechowywać wszystko, co chcesz, o ile jest wystarczająco małe i prawidłowo radzisz sobie z wyrównaniem.
źródło
std::aligned_storage
, dzięki! :)std::aligned_storage<...>::type
jest tylko surowym buforem, który w przeciwieństwie do tegochar [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 (zanew
pomocąconstruct
metody umieszczania lub alokatora ) i musisz ręcznie zniszczyć również obiekty w nim (ręcznie wywołując ich destruktor lub używającdestroy
metody alokatora ).Stroustrup, w języku programowania C ++ (wydanie czwarte) §25.3 , stwierdza:
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 Vector
kontenera na wzórstd::vector
. Kiedy będziesz używał swojegoVector
z 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 podstawyVector<T*>
dla wszystkich innych typówT
: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łuvoid*
.źródło
template<typename Derived> VectorBase<Derived>
która jest następnie wyspecjalizowana jakotemplate<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.Zobacz tę serię postów, aby zapoznać się z (dość krótką) listą technik usuwania czcionek i dyskusją na temat kompromisów: Część I , Część II , Część III , Część IV .
Ten, o którym jeszcze nie wspomniałem, to Adobe.Poly i Boost.Variant , które można w pewnym stopniu uznać za wymazywanie typu.
źródło
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:źródło