c++ - tutorial - Escriba las técnicas de borrado
sintaxis de c++ (6)
Como dice Marc, uno puede usar cast std::shared_ptr<void>
. Por ejemplo, almacene el tipo en un puntero de función, moldee y almacene en un funtor de 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
// !
}
(Con el borrado de tipo, me refiero a ocultar algunos o todos los tipos de información con respecto a una clase, algo así como Boost.Any .)
Quiero conocer las técnicas de borrado de tipos, al mismo tiempo que las comparto, lo cual sé. Mi esperanza es encontrar alguna técnica loca en la que alguien haya pensado en su hora más oscura. :)
El primero y más obvio, y el enfoque comúnmente adoptado, que yo sé, son funciones virtuales. Simplemente oculte la implementación de su clase dentro de una jerarquía de clases basada en la interfaz. Muchas bibliotecas de Boost hacen esto, por ejemplo, Boost.Any hace esto para ocultar su tipo y Boost.Shared_ptr hace esto para ocultar la mecánica de asignación (de).
Luego está la opción con punteros de función para funciones con plantilla, mientras se mantiene el objeto real en un puntero void*
, como Boost.Function para ocultar el tipo real del funtor. Las implementaciones de ejemplo se pueden encontrar al final de la pregunta.
Entonces, para mi pregunta real:
¿Qué otras técnicas de borrado de tipo conoces? Proporcione, de ser posible, un código de ejemplo, casos de uso, su experiencia con ellos y, tal vez, enlaces para seguir leyendo.
Editar
(Dado que no estaba seguro de si agregar esto como respuesta, o simplemente editar la pregunta, haré la más segura).
Otra buena técnica para ocultar el tipo real de algo sin funciones virtuales o violín void*
es la que GMan emplea here , con relevancia para mi pregunta sobre cómo funciona exactamente esto.
Código de ejemplo:
#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();
}
Consulte esta serie de publicaciones para obtener una lista (bastante breve) de técnicas de borrado de tipos y la discusión sobre las compensaciones: Parte I , Parte II , Parte III , Parte IV .
El que no he visto mencionar aún es Adobe.Poly y Boost.Variant , que se puede considerar una especie de borrado hasta cierto punto.
Fundamentalmente, esas son sus opciones: funciones virtuales o indicadores de función.
Cómo puede almacenar los datos y asociarlos con las funciones puede variar. Por ejemplo, puede almacenar un puntero a la base y hacer que la clase derivada contenga los datos y las implementaciones de la función virtual, o puede almacenar los datos en otro lugar (por ejemplo, en un búfer asignado por separado) y solo proporcionar la clase derivada las implementaciones de funciones virtuales, que tienen un void*
que apunta a los datos. Si almacena los datos en un búfer separado, puede usar punteros a funciones en lugar de funciones virtuales.
Almacenar un puntero a base funciona bien en este contexto, incluso si los datos se almacenan por separado, si hay varias operaciones que desea aplicar a sus datos borrados por tipo. De lo contrario, terminará con múltiples punteros de función (uno para cada una de las funciones borradas por tipo), o funciones con un parámetro que especifica la operación a realizar.
Stroustrup, en el lenguaje de programación C ++ (4ª edición) §25.3 , declara:
Las variantes de la técnica de utilizar una única representación en tiempo de ejecución para valores de varios tipos y confiar en el sistema de tipo (estático) para garantizar que se usan solo de acuerdo con su tipo declarado se han denominado borrado de tipo .
En particular, no se necesita el uso de funciones virtuales o indicadores de función para realizar borrado de tipo si usamos plantillas. El caso, ya mencionado en otras respuestas, de la llamada al destructor correcto de acuerdo con el tipo almacenado en std::shared_ptr<void>
es un ejemplo de eso.
El ejemplo proporcionado en el libro de Stroustrup es igual de agradable.
Piensa en implementar template<class T> class Vector
, un contenedor a lo largo de las líneas de std::vector
. Cuando utilice su Vector
con muchos tipos de punteros diferentes, como suele ocurrir, el compilador generará distintos códigos para cada tipo de puntero.
Este engrosamiento del código se puede evitar definiendo una especialización de Vector para los punteros void*
y luego utilizando esta especialización como una implementación básica común de Vector<T*>
para todos los demás tipos 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)); }
};
Como puede ver, tenemos un contenedor fuertemente tipado, pero Vector<Animal*>
, Vector<Dog*>
, Vector<Cat*>
, ... compartirá el mismo código (C ++ y binario) para la implementación, teniendo su tipo de puntero borrado detrás del void*
.
También consideraría (similar a void*
) el uso de "almacenamiento en bruto": char buffer[N]
.
En C ++ 0x tiene std::aligned_storage<Size,Align>::type
para esto.
Puede almacenar lo que quiera allí, siempre que sea lo suficientemente pequeño y trate la alineación correctamente.
Todas las técnicas de borrado de caracteres en C ++ se realizan con punteros de función (para el comportamiento) y void*
(para los datos). Los "diferentes" métodos simplemente difieren en la forma en que agregan el azúcar semántico. Las funciones virtuales, por ejemplo, son solo azúcar semántica para
struct Class {
struct vtable {
void (*dtor)(Class*);
void (*func)(Class*,double);
} * vtbl
};
iow: punteros de función.
Sin embargo, hay una técnica que me gusta particularmente: es shared_ptr<void>
, simplemente porque deja shared_ptr<void>
personas que no saben que puedes hacer esto: puedes almacenar cualquier información en un shared_ptr<void>
, y todavía tiene el destructor correcto llamado al final, porque el constructor shared_ptr
es una plantilla de función, y usará el tipo de objeto real pasado para crear el eliminador de forma predeterminada:
{
const shared_ptr<void> sp( new A );
} // calls A::~A() here
Por supuesto, esto es simplemente el borrado de tipo void*
/ function-pointer habitual, pero muy bien empaquetado.