Técnicas de apagamento de tipo

136

(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();
}
Xeo
fonte
1
Por "apagamento de tipo", você está realmente se referindo ao "polimorfismo"? Eu acho que "apagamento de tipo" tem um significado um tanto específico, que geralmente é associado a, por exemplo, genéricos Java.
Oliver Charlesworth 27/03
3
@Oli: O apagamento de tipo pode ser implementado com polimorfismo, mas essa não é a única opção, meu segundo exemplo mostra isso. :) E com apagamento de tipo, quero dizer apenas que sua estrutura não depende de um tipo de modelo, por exemplo. Boost.Function não se importa se você alimenta um functor, um ponteiro de função ou mesmo um lambda. Mesmo com Boost.Shared_Ptr. Você pode especificar uma função de alocador e desalocação, mas o tipo real do shared_ptrnão reflete isso, sempre será o mesmo, shared_ptr<int>por exemplo, ao contrário do contêiner padrão.
Xeo 27/03
2
@ Matthieu: Considero o segundo exemplo também o tipo seguro. Você sempre sabe o tipo exato em que está operando. Ou eu estou esquecendo de alguma coisa?
Xeo 27/03
2
@ Matthieu: Você está certo. Normalmente, essa Asfunção não seria implementada dessa maneira. Como eu disse, de maneira alguma é seguro de usar! :)
Xeo
4
@ lurscher: Bem ... nunca usou as versões boost ou std de qualquer um dos seguintes? function, shared_ptr, any, Etc.? Todos eles empregam apagamento de tipo para conveniência do usuário.
Xeo

Respostas:

100

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 para

struct Class {
    struct vtable {
        void (*dtor)(Class*);
        void (*func)(Class*,double);
    } * vtbl
};

iow: 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 um shared_ptr<void>e ainda assim chamar o destruidor correto no fim, porque o shared_ptrconstrutor é um modelo de função e usará o tipo do objeto real passado para criar o deleter por padrão:

{
    const shared_ptr<void> sp( new A );
} // calls A::~A() here

Obviamente, isso é apenas o void*apagamento usual / tipo de ponteiro de função, mas é muito conveniente.

Marc Mutz - mmutz
fonte
9
Coincidentemente, tive que explicar o comportamento de shared_ptr<void>um amigo meu com um exemplo de implementação apenas alguns dias atrás. :) É realmente legal.
Xeo 18/05
Boa resposta; para torná-lo incrível, é muito educativo um esboço de como uma tabela falsa pode ser criada de forma estática para cada tipo apagado. Observe que as implementações de falso-vtables e de ponteiro de função fornecem estruturas conhecidas do tamanho da memória (em comparação com tipos puramente virtuais) que podem ser facilmente armazenadas localmente e (facilmente) divorciadas dos dados que estão virtualizando.
precisa saber é o seguinte
portanto, se o shared_ptr, em seguida, armazena um Derivado *, mas a Base * não declara o destruidor como virtual, shared_ptr <void> ainda funciona como pretendido, pois nunca soube da classe base para começar. Legal!
TamaMcGlinn
@ Apollys: Sim, mas unique_ptrnão apaga o deletador, por isso, se você deseja atribuir um unique_ptr<T>a a unique_ptr<void>, é necessário fornecer um argumento deletador, explicitamente, que saiba como excluir o Ta void*. Se agora você deseja atribuir um S, também, então você precisa de um deleter, explicitamente, que sabe como eliminar um Tpor um void*e também um Satravés de um void*, e , dado um void*, sabe se é um Tou um S. Nesse ponto, você escreveu um deleter apagado por tipo para unique_ptre, em seguida, ele também funciona unique_ptr. Apenas não fora da caixa.
Marc Mutz - mmutz 17/09/19
Sinto que a pergunta que você respondeu foi "Como solucionar o fato de que isso não funciona 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.
Apollys suporta Monica
54

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.

Anthony Williams
fonte
1
Então, em outras palavras, os exemplos que dei na pergunta? No entanto, obrigado por escrevê-lo dessa maneira, especialmente nas funções virtuais e nas várias operações nos dados apagados por tipo.
Xeo 17/05
Existem pelo menos 2 outras opções. Estou compondo uma resposta.
John Dibling
25

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>::typepara isso.

Você pode armazenar o que quiser lá, desde que seja pequeno o suficiente e lide adequadamente com o alinhamento.

Matthieu M.
fonte
4
Bem, sim, o Boost.Function realmente usa uma combinação disso e do segundo exemplo que eu dei. Se o functor for pequeno o suficiente, ele será armazenado internamente dentro do functor_buffer. É bom saber sobre isso std::aligned_storage, obrigado! :)
Xeo 27/03
Você também pode usar o canal novo para isso.
Rustyx 28/08
2
@RustyX: Na verdade, você precisa . std::aligned_storage<...>::typeé apenas um buffer bruto que, ao contrário char [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 posicionamento newou constructmétodo de alocador ) e também destruir manualmente os objetos dentro dele (invocar manualmente o destruidor ou usar um destroymétodo de alocador ) )
Matthieu M.
22

Stroustrup, na linguagem de programação C ++ (4ª edição) §25.3 , declara:

Variantes da técnica de usar uma única representação em tempo de execução para valores de vários tipos e confiar no sistema de tipos (estático) para garantir que eles sejam usados ​​apenas de acordo com o tipo declarado foram denominados apagamento de tipo .

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 de std::vector. Quando você usará o seu Vectorcom 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 comum Vector<T*>para todos os outros 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 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ás void*.

Paolo M
fonte
2
Sem querer ser blasfêmia: eu preferiria o CRTP à técnica dada por Stroustrup.
Davidhigh
@davidhigh O que você quer dizer?
Paola M
Pode-se obter o mesmo comportamento (com uma sintaxe menos desagradável) usando uma classe base CRTPtemplate<typename Derived> VectorBase<Derived> que é então especializada como template<typename T> VectorBase<Vector<T*> >. Além disso, essa abordagem não funciona apenas para ponteiros, mas para qualquer tipo.
Davidhigh 23/11/2015
3
Observe que bons vinculadores de C ++ mesclam métodos e funções idênticas: o vinculador de ouro ou dobragem de comdat do MSVC. O código é gerado, mas depois descartado durante a vinculação.
precisa saber é o seguinte
1
@davidhigh Estou tentando entender seu comentário e me pergunto se você pode me fornecer um link ou o nome de um padrão para o qual pesquisar (não o CRTP, mas o nome de uma técnica que permite o apagamento de tipo sem funções virtuais ou ponteiros de função) . Respeitosamente, - Chris
Chris Chiasson
19

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.

Andrzej
fonte
7

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:

#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
    // !
}
Janek Olszak
fonte