(Avec l'effacement de type, je veux dire masquer tout ou partie des informations de type concernant une classe, un peu comme Boost.Any .)
Je veux me familiariser avec les techniques d'effacement de caractères, tout en partageant celles que je connais. Mon espoir est un peu de trouver une technique folle à laquelle quelqu'un a pensé à son heure la plus sombre. :)
La première approche, la plus évidente et la plus couramment adoptée, que je connaisse, sont les fonctions virtuelles. Cachez simplement l'implémentation de votre classe dans une hiérarchie de classes basée sur une interface. De nombreuses bibliothèques Boost le font, par exemple Boost.Any le fait pour masquer votre type et Boost.Shared_ptr le fait pour masquer le (dé) mécanisme d'allocation.
Ensuite, il y a l'option avec des pointeurs de fonction vers des fonctions de modèle, tout en maintenant l'objet réel dans un void*
pointeur, comme Boost.Function fait pour masquer le type réel du foncteur. Des exemples d'implémentations se trouvent à la fin de la question.
Donc, pour ma vraie question:
Quelles autres techniques d'effacement connaissez-vous? Veuillez leur fournir, si possible, un exemple de code, des cas d'utilisation, votre expérience avec eux et peut-être des liens pour une lecture plus approfondie.
Modifier
(Étant donné que je n'étais pas sûr d'ajouter ceci comme réponse, ou simplement de modifier la question, je ferai juste la plus sûre.)
Une autre belle technique pour masquer le type réel de quelque chose sans fonctions virtuelles ou void*
le violon, est celui que GMan emploie ici , avec une pertinence pour ma question sur la façon dont cela fonctionne exactement.
Exemple de code:
#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();
}
Toutes les techniques d'effacement de types en C++ sont effectuées avec des pointeurs de fonction (pour le comportement) et void*
(Pour les données). Les méthodes "différentes" diffèrent simplement par la façon dont elles ajoutent du sucre sémantique. Les fonctions virtuelles, par exemple, ne sont que du sucre sémantique pour
struct Class {
struct vtable {
void (*dtor)(Class*);
void (*func)(Class*,double);
} * vtbl
};
iow: pointeurs de fonction.
Cela dit, il y a une technique que j'aime particulièrement, cependant: c'est shared_ptr<void>
, Simplement parce qu'elle époustoufle les gens qui ne savent pas que vous pouvez le faire: vous pouvez stocker toutes les données dans un shared_ptr<void>
, et toujours avoir le destructeur correct appelé à la fin, car le constructeur shared_ptr
est un modèle de fonction, et utilisera le type de l'objet réel passé pour créer le suppresseur par défaut:
{
const shared_ptr<void> sp( new A );
} // calls A::~A() here
Bien sûr, il s'agit simplement de l'effacement de type void*
/Fonction-pointeur habituel, mais très pratique.
Fondamentalement, ce sont vos options: fonctions virtuelles ou pointeurs de fonction.
La façon dont vous stockez les données et les associez aux fonctions peut varier. Par exemple, vous pouvez stocker un pointeur vers la base et faire en sorte que la classe dérivée contienne les données et les implémentations des fonctions virtuelles, ou vous pouvez stocker les données ailleurs (par exemple dans un tampon alloué séparément), et que la classe dérivée fournisse les implémentations de fonctions virtuelles, qui prennent un void*
qui pointe vers les données. Si vous stockez les données dans un tampon séparé, vous pouvez utiliser des pointeurs de fonction plutôt que des fonctions virtuelles.
Le stockage d'un pointeur vers la base fonctionne bien dans ce contexte, même si les données sont stockées séparément, s'il existe plusieurs opérations que vous souhaitez appliquer à vos données effacées par type. Sinon, vous vous retrouvez avec plusieurs pointeurs de fonction (un pour chacune des fonctions effacées) ou des fonctions avec un paramètre qui spécifie l'opération à effectuer.
Je considérerais également (similaire à void*
) l'utilisation du "stockage brut": char buffer[N]
.
En C++ 0x, vous avez std::aligned_storage<Size,Align>::type
pour ça.
Vous pouvez y stocker tout ce que vous voulez, tant qu'il est assez petit et que vous gérez correctement l'alignement.
Stroustrup, dans Le langage de programmation C++ (4e édition) §25.3 , déclare:
Des variantes de la technique consistant à utiliser une seule représentation d'exécution pour les valeurs d'un certain nombre de types et à s'appuyer sur le système de type (statique) pour garantir qu'elles ne sont utilisées qu'en fonction de leur type déclaré ont été appelées effacez le type .
En particulier, pas d'utilisation de fonctions virtuelles ou de pointeurs de fonction est nécessaire pour effectuer l'effacement de type si nous utilisons des modèles. Le cas, déjà mentionné dans d'autres réponses, de l'appel destructeur correct selon le type stocké dans un std::shared_ptr<void>
en est un exemple.
L'exemple fourni dans le livre de Stroustrup est tout aussi agréable.
Pensez à implémenter template<class T> class Vector
, un conteneur du type std::vector
. Lorsque vous utiliserez votre Vector
avec de nombreux types de pointeurs différents, comme cela arrive souvent, le compilateur générera soi-disant un code différent pour chaque type de pointeur.
Ce gonflement du code peut être évité en définissant une spécialisation du vecteur pour void*
pointeurs, puis en utilisant cette spécialisation comme implémentation de base commune de Vector<T*>
pour tous les autres types 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)); }
};
Comme vous pouvez le voir, nous avons un conteneur fortement typé mais Vector<Animal*>
, Vector<Dog*>
, Vector<Cat*>
, ..., partagera le même code (C++ et binaire) pour l'implémentation, en ayant leur type de pointeur effacé derrière void*
.
Voir cette série d'articles pour une liste (assez courte) des techniques d'effacement de type et la discussion sur les compromis: Partie I , Partie II , Partie III , Partie IV .
Celui que je n'ai pas encore vu mentionné est Adobe.Poly , et Boost.Variant , qui peut être considéré comme un effacement de type dans une certaine mesure.
Comme indiqué par Marc, on peut utiliser cast std::shared_ptr<void>
. Par exemple, stockez le type dans un pointeur de fonction, convertissez-le et stockez-le dans un foncteur d'un seul type:
#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
// !
}