(Mit Typlöschung meine ich, einige oder alle Typinformationen zu einer Klasse zu verbergen , ähnlich wie Boost.Any .)
Ich möchte die Techniken zum Löschen von Typen kennenlernen und gleichzeitig die mir bekannten teilen. Meine Hoffnung ist es, eine verrückte Technik zu finden, an die jemand in seiner dunkelsten Stunde gedacht hat. :) :)
Der erste und offensichtlichste und am häufigsten verwendete Ansatz, den ich kenne, sind virtuelle Funktionen. Verstecken Sie einfach die Implementierung Ihrer Klasse in einer schnittstellenbasierten Klassenhierarchie. Viele Boost-Bibliotheken tun dies, zum Beispiel Boost.Any tut dies, um Ihren Typ auszublenden, und Boost.Shared_ptr tut dies, um die ( De- ) Zuordnungsmechanik auszublenden.
Dann gibt es die Option mit Funktionszeigern auf Vorlagenfunktionen, während das eigentliche Objekt in einem void*
Zeiger gehalten wird, wie dies Boost.Function tut, um den realen Typ des Funktors auszublenden. Beispielimplementierungen finden Sie am Ende der Frage.
Also, für meine eigentliche Frage:
Welche anderen Löschtechniken kennen Sie? Bitte geben Sie ihnen nach Möglichkeit einen Beispielcode, Anwendungsfälle, Ihre Erfahrungen mit ihnen und möglicherweise Links zur weiteren Lektüre.
Bearbeiten
(Da ich nicht sicher war, ob ich dies als Antwort hinzufügen oder nur die Frage bearbeiten sollte, mache ich einfach die sicherere.)
Eine andere nette Technik, um den tatsächlichen Typ von etwas ohne virtuelle Funktionen oder void*
Fummelei zu verbergen , ist das Ein GMan beschäftigt hier , was für meine Frage relevant ist, wie genau dies funktioniert.
Beispielcode:
#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();
}
quelle
shared_ptr
spiegelt dies nicht wider. Er istshared_ptr<int>
beispielsweise im Gegensatz zum Standardcontainer immer derselbe .As
Funktion nicht auf diese Weise implementiert. Wie gesagt, keineswegs sicher zu bedienen! :)function
,shared_ptr
,any
Etc.? Sie alle verwenden Typlöschung für süße, süße Benutzerkomfort.Antworten:
Alle Löschtechniken in C ++ werden mit Funktionszeigern (für das Verhalten) und
void*
(für Daten) ausgeführt. Die "verschiedenen" Methoden unterscheiden sich einfach darin, wie sie semantischen Zucker hinzufügen. Virtuelle Funktionen sind zB nur semantischer Zucker füriow: Funktionszeiger.
Das heißt, es gibt eine Technik, die ich besonders mag: Es ist
shared_ptr<void>
einfach, weil sie Leute um den Verstand bringt, die nicht wissen, dass Sie dies tun können: Sie können alle Daten in einem speichernshared_ptr<void>
und haben immer noch den richtigen Destruktor auf dem Ende, da dershared_ptr
Konstruktor eine Funktionsvorlage ist und standardmäßig den Typ des tatsächlich übergebenen Objekts zum Erstellen des Löschers verwendet:Dies ist natürlich nur die übliche
void*
Löschung vom Typ / Funktionszeiger, aber sehr bequem verpackt.quelle
shared_ptr<void>
einigen Tagen einem Freund von mir das Verhalten anhand einer Beispielimplementierung erklären . :) Es ist wirklich cool.unique_ptr
Der Deleter wird zwar gelöscht, aber nicht typisiert. Wenn Sie also aunique_ptr<T>
einem zuweisen möchtenunique_ptr<void>
, müssen Sie explizit ein Deleter-Argument angeben, das weiß, wie dasT
durch a gelöscht wirdvoid*
. Wenn Sie jetzt ein zuweisen möchtenS
, auch dann müssen Sie eine deleter, explizit, die weiß , wie ein löschenT
durch einvoid*
und auch einS
durch einvoid*
, und , da einvoid*
, weiß , ob es ein istT
oder einS
. Zu diesem Zeitpunkt haben Sie einen typenlöschenden Löscher für geschriebenunique_ptr
, und dann funktioniert er auch fürunique_ptr
. Nur nicht sofort einsatzbereit.unique_ptr
?" Nützlich für einige Leute, hat aber meine Frage nicht beantwortet. Ich denke, die Antwort ist, weil gemeinsame Zeiger bei der Entwicklung der Standardbibliothek mehr Aufmerksamkeit erhielten. Was ich ein wenig traurig finde, weil eindeutige Zeiger einfacher sind, daher sollte es einfacher sein, grundlegende Funktionen zu implementieren, und sie sind effizienter, sodass die Benutzer sie häufiger verwenden sollten. Stattdessen haben wir genau das Gegenteil.Grundsätzlich sind dies Ihre Optionen: virtuelle Funktionen oder Funktionszeiger.
Wie Sie die Daten speichern und mit den Funktionen verknüpfen, kann variieren. Sie könnten beispielsweise einen Zeiger auf die Basis speichern und die abgeleitete Klasse die Daten und die Implementierungen der virtuellen Funktionen enthalten lassen, oder Sie könnten die Daten an anderer Stelle speichern (z. B. in einem separat zugewiesenen Puffer) und nur die abgeleitete Klasse bereitstellen lassen die virtuellen Funktionsimplementierungen, die
void*
auf die Daten verweisen. Wenn Sie die Daten in einem separaten Puffer speichern, können Sie anstelle virtueller Funktionen Funktionszeiger verwenden.Das Speichern eines Zeigers auf die Basis funktioniert in diesem Zusammenhang gut, auch wenn die Daten separat gespeichert werden, wenn Sie mehrere Operationen auf Ihre typgelöschten Daten anwenden möchten. Andernfalls erhalten Sie mehrere Funktionszeiger (einen für jede der vom Typ gelöschten Funktionen) oder Funktionen mit einem Parameter, der die auszuführende Operation angibt.
quelle
Ich würde auch (ähnlich
void*
) die Verwendung von "Rohspeicher" in Betracht ziehen :char buffer[N]
.In C ++ 0x haben Sie
std::aligned_storage<Size,Align>::type
dafür.Sie können dort alles speichern, was Sie möchten, solange es klein genug ist und Sie mit der Ausrichtung richtig umgehen.
quelle
std::aligned_storage
, danke! :)std::aligned_storage<...>::type
ist nur ein Rohpuffer, der im Gegensatzchar [sizeof(T)]
dazu geeignet ausgerichtet ist. An sich ist es jedoch inert: Es initialisiert seinen Speicher nicht, baut kein Objekt auf, nichts. Sobald Sie einen Puffer dieses Typs haben, müssen Sie Objekte darin manuell erstellen (entweder mit Platzierungnew
oder einer Zuweisungsmethodeconstruct
) und Sie müssen auch die darin enthaltenen Objekte manuell zerstören (entweder manuell ihren Destruktor aufrufen oder eine Zuweisungsmethodedestroy
verwenden ).Stroustrup heißt in der Programmiersprache C ++ (4. Ausgabe) §25.3 :
Insbesondere ist keine Verwendung von virtuellen Funktionen oder Funktionszeigern erforderlich, um das Löschen von Typen durchzuführen, wenn Vorlagen verwendet werden. Der bereits in anderen Antworten erwähnte Fall des richtigen Destruktoraufrufs gemäß dem in a gespeicherten Typ
std::shared_ptr<void>
ist ein Beispiel dafür.Das Beispiel in Stroustrups Buch ist genauso unterhaltsam.
Denken Sie an die Implementierung
template<class T> class Vector
eines Containers nach dem Vorbild vonstd::vector
. Wenn Sie IhrenVector
mit vielen verschiedenen Zeigertypen verwenden, wie es häufig vorkommt, generiert der Compiler angeblich für jeden Zeigertyp einen anderen Code.Dieses Aufblähen des Codes kann verhindert werden, indem eine Spezialisierung des Vektors für
void*
Zeiger definiert und diese Spezialisierung dann als gemeinsame BasisimplementierungVector<T*>
für alle anderen Typen verwendet wirdT
:Wie Sie sehen können, haben wir einen stark typisierte Container aber
Vector<Animal*>
,Vector<Dog*>
,Vector<Cat*>
, ..., das gleiche (C ++ teilen und binär) Code für die Implementierung, dessen Zeiger Typen gelöscht hintervoid*
.quelle
template<typename Derived> VectorBase<Derived>
die dann als spezialisiert isttemplate<typename T> VectorBase<Vector<T*> >
. Darüber hinaus funktioniert dieser Ansatz nicht nur für Zeiger, sondern für jeden Typ.In dieser Reihe von Beiträgen finden Sie eine (ziemlich kurze) Liste der Löschtechniken und die Diskussion über die Kompromisse: Teil I , Teil II , Teil III , Teil IV .
Die, die ich noch nicht erwähnt habe, ist Adobe.Poly und Boost.Variant , die bis zu einem gewissen Grad als Typlöschung angesehen werden können.
quelle
Wie von Marc angegeben, kann man Besetzung verwenden
std::shared_ptr<void>
. Speichern Sie beispielsweise den Typ in einem Funktionszeiger, wandeln Sie ihn um und speichern Sie ihn in einem Funktor nur eines Typs:quelle