(Com o apagamento de tipo, quero dizer ocultar algumas ou todas as informações de tipo sobre uma classe, como Boost.Any .)
Quero conhecer as técnicas de apagamento de tipo, além de compartilhar as que eu conheço. Minha esperança é encontrar uma técnica maluca que alguém tenha pensado em sua hora mais sombria. :)
A primeira e mais óbvia, e comumente adotada abordagem, que eu sei, são funções virtuais. Apenas oculte a implementação de sua classe dentro de uma hierarquia de classes baseada em interface. Muitas bibliotecas Boost fazem isso, por exemplo, Boost.Any faz isso para ocultar seu tipo e Boost.Shared_ptr faz isso para ocultar a mecânica de (des) alocação.
Depois, há a opção com ponteiros de função para funções de modelo, enquanto mantém o objeto real em um void*
ponteiro, como o Boost.Function faz para ocultar o tipo real do functor. Implementações de exemplo podem ser encontradas no final da pergunta.
Então, para minha pergunta real:
Que outras técnicas de apagamento você conhece? Forneça, se possível, um código de exemplo, casos de uso, sua experiência com eles e talvez links para leitura adicional.
Editar
(Como eu não tinha certeza se gostaria de adicionar isso como resposta ou apenas editar a pergunta, farei a mais segura.)
Outra técnica interessante para ocultar o tipo real de algo sem funções virtuais ou void*
mexer, é o um GMan emprega aqui , com relevância para a minha pergunta sobre como exatamente isso funciona.
Código de exemplo:
#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();
}
fonte
shared_ptr
não reflete isso, sempre será o mesmo,shared_ptr<int>
por exemplo, ao contrário do contêiner padrão.As
função não seria implementada dessa maneira. Como eu disse, de maneira alguma é seguro de usar! :)function
,shared_ptr
,any
, Etc.? Todos eles empregam apagamento de tipo para conveniência do usuário.Respostas:
Todas as técnicas de apagamento de tipo em C ++ são feitas com ponteiros de função (para comportamento) e
void*
(para dados). Os métodos "diferentes" simplesmente diferem na maneira como adicionam açúcar semântico. Funções virtuais, por exemplo, são apenas açúcar semântico paraiow: ponteiros de função.
Dito isto, há uma técnica de que particularmente gosto: é
shared_ptr<void>
simplesmente porque isso desanima as pessoas que não sabem que você pode fazer isso: você pode armazenar qualquer dado em umshared_ptr<void>
e ainda assim chamar o destruidor correto no fim, porque oshared_ptr
construtor é um modelo de função e usará o tipo do objeto real passado para criar o deleter por padrão:Obviamente, isso é apenas o
void*
apagamento usual / tipo de ponteiro de função, mas é muito conveniente.fonte
shared_ptr<void>
um amigo meu com um exemplo de implementação apenas alguns dias atrás. :) É realmente legal.unique_ptr
não apaga o deletador, por isso, se você deseja atribuir umunique_ptr<T>
a aunique_ptr<void>
, é necessário fornecer um argumento deletador, explicitamente, que saiba como excluir oT
avoid*
. Se agora você deseja atribuir umS
, também, então você precisa de um deleter, explicitamente, que sabe como eliminar umT
por umvoid*
e também umS
através de umvoid*
, e , dado umvoid*
, sabe se é umT
ou umS
. Nesse ponto, você escreveu um deleter apagado por tipo paraunique_ptr
e, em seguida, ele também funcionaunique_ptr
. Apenas não fora da caixa.unique_ptr
?" Útil para algumas pessoas, mas não respondeu à minha pergunta. Eu acho que a resposta é, porque ponteiros compartilhados receberam mais atenção no desenvolvimento da biblioteca padrão. O que eu acho um pouco triste, porque ponteiros únicos são mais simples, por isso deve ser mais fácil implementar funcionalidades básicas e mais eficientes para que as pessoas os usem mais. Em vez disso, temos exatamente o oposto.Fundamentalmente, essas são suas opções: funções virtuais ou ponteiros de função.
Como você armazena os dados e os associa às funções pode variar. Por exemplo, você pode armazenar um ponteiro para a base e fazer com que a classe derivada contenha os dados e as implementações da função virtual, ou você pode armazenar os dados em outro local (por exemplo, em um buffer alocado separadamente) e apenas fornecer a classe derivada as implementações da função virtual,
void*
que apontam para os dados. Se você armazenar os dados em um buffer separado, poderá usar ponteiros de função em vez de funções virtuais.O armazenamento de um ponteiro para a base funciona bem nesse contexto, mesmo que os dados sejam armazenados separadamente, se houver várias operações que você deseja aplicar aos dados apagados por tipo. Caso contrário, você terminará com vários ponteiros de função (um para cada uma das funções apagadas por tipo) ou funções com um parâmetro que especifica a operação a ser executada.
fonte
Gostaria também de considerar (semelhante a
void*
) o uso de "armazenamento bruto":char buffer[N]
.No C ++ 0x você tem
std::aligned_storage<Size,Align>::type
para isso.Você pode armazenar o que quiser lá, desde que seja pequeno o suficiente e lide adequadamente com o alinhamento.
fonte
std::aligned_storage
, obrigado! :)std::aligned_storage<...>::type
é apenas um buffer bruto que, ao contráriochar [sizeof(T)]
, está adequadamente alinhado. Por si só, porém, é inerte: não inicializa sua memória, não constrói um objeto, nada. Portanto, depois de ter um buffer desse tipo, você deve construir manualmente objetos dentro dele (com posicionamentonew
ouconstruct
método de alocador ) e também destruir manualmente os objetos dentro dele (invocar manualmente o destruidor ou usar umdestroy
método de alocador ) )Stroustrup, na linguagem de programação C ++ (4ª edição) §25.3 , declara:
Em particular, nenhum uso de funções virtuais ou ponteiros de função é necessário para executar o apagamento de tipo se usarmos modelos. O caso, já mencionado em outras respostas, da chamada correta do destruidor de acordo com o tipo armazenado em a
std::shared_ptr<void>
é um exemplo disso.O exemplo fornecido no livro de Stroustrup é igualmente agradável.
Pense em implementar
template<class T> class Vector
, um contêiner ao longo das linhas destd::vector
. Quando você usará o seuVector
com muitos tipos diferentes de ponteiros, como geralmente acontece, o compilador gerará códigos diferentes para cada tipo de ponteiro.Esse inchaço do código pode ser evitado definindo uma especialização Vector para
void*
ponteiros e, em seguida, usando essa especialização como uma implementação base comumVector<T*>
para todos os outros tiposT
:Como você pode ver, temos um recipiente fortemente tipado, mas
Vector<Animal*>
,Vector<Dog*>
,Vector<Cat*>
, ..., irá compartilhar a mesma (C ++ e código para a implementação binário), tendo o seu tipo de ponteiro apagados trásvoid*
.fonte
template<typename Derived> VectorBase<Derived>
que é então especializada comotemplate<typename T> VectorBase<Vector<T*> >
. Além disso, essa abordagem não funciona apenas para ponteiros, mas para qualquer tipo.Veja esta série de postagens para obter uma lista (bastante curta) de técnicas de apagamento de tipo e a discussão sobre as compensações: Parte I , Parte II , Parte III , Parte IV .
O que eu ainda não vi mencionado é o Adobe.Poly e o Boost.Variant , que podem ser considerados um apagamento de tipo até certo ponto.
fonte
Como afirma Marc, pode-se usar elenco
std::shared_ptr<void>
. Por exemplo, armazene o tipo em um ponteiro de função, faça a conversão e armazene em um functor de apenas um tipo:fonte