Методы стирания типа

(С стиранием типа я имею в виду скрывать некоторую или всю информацию о типе относительно класса, несколько похожую на Boost.Any. )
Я хочу получить методы стирания типа, а также поделиться теми, о которых я знаю. Моя надежда - это найти какую-то сумасшедшую технику, о которой кто-то думал в свой темный час.:)

Первый и наиболее очевидный и общепринятый подход, который я знаю, являются виртуальными функциями. Просто скройте реализацию своего класса внутри иерархии классов на основе интерфейса. Многие библиотеки Boost делают это, например Boost.Any скрывает ваш тип и Boost.Shared_ptr делает это, чтобы скрыть механизм выделения (de).

Затем есть опция с указателями функций для шаблонных функций, удерживая фактический объект в указателе void*, например Boost.Function делает, чтобы скрыть реальный тип функтора. Примеры реализации можно найти в конце вопроса.

Итак, для моего фактического вопроса:
Какие еще методы стирания типа вы знаете? Пожалуйста, предоставьте им, если это возможно, примерный код, варианты использования, ваш опыт работы с ними и, возможно, ссылки для дальнейшего чтения.

Edit
(Поскольку я не был уверен, что могу добавить это как ответ или просто отредактировать вопрос, я просто сделаю более безопасный).
Еще одна хорошая техника для скрытия фактического типа виртуальных функций без или void* fiddling, является тем, что GMan использует здесь, что имеет значение для мой вопрос о том, как именно это работает.


Пример кода:

#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();
}

Ответ 1

Все методы стирания стилей в С++ выполняются с указателями функций (для поведения) и void* (для данных). "Различные" методы просто отличаются тем, как они добавляют семантический сахар. Виртуальные функции, например, являются просто семантическим сахаром для

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

iow: указатели на функции.

Тем не менее, есть один метод, который мне особенно нравится: shared_ptr<void>, просто потому, что он удаляет умы людей, которые не знают, что вы можете это сделать: вы можете хранить любые данные в shared_ptr<void>, и по-прежнему есть правильный деструктор, вызываемый в конце, потому что конструктор shared_ptr является шаблоном функции и будет использовать тип фактического объекта, переданного для создания делетера по умолчанию:

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

Конечно, это обычное стирание типа void*/function-pointer, но очень удобно упаковано.

Ответ 2

По сути, это ваши варианты: виртуальные функции или указатели функций.

Как вы храните данные и связываете их с функциями, которые могут отличаться. Например, вы можете хранить указатель на базу и иметь производный класс, содержащий данные и реализации виртуальных функций, или вы можете хранить данные в другом месте (например, в отдельно распределенном буфере), и просто иметь производный класс, обеспечивающий реализацию виртуальных функций, которые принимают void*, который указывает на данные. Если вы храните данные в отдельном буфере, вы можете использовать указатели функций, а не виртуальные функции.

Сохранение указателя на базу хорошо работает в этом контексте, даже если данные хранятся отдельно, если есть несколько операций, которые вы хотите применить к данным, удаленным по типу. В противном случае вы получите несколько указателей на функции (по одному для каждой из стираемых по типу функций) или функций с параметром, который определяет выполняемую операцию.

Ответ 3

Я бы также рассмотрел (аналогично void*) использование "raw storage": char buffer[N].

В С++ 0x для этого есть std::aligned_storage<Size,Align>::type.

Вы можете хранить все, что хотите, там, пока оно достаточно мало, и вы правильно относитесь к выравниванию.

Ответ 4

Stroustrup, на языке программирования С++ (четвертое издание) §25.3, говорится:

Варианты метода использования одного представления времени выполнения для значений нескольких типов и использования системы статического типа для обеспечения того, чтобы они использовались только в соответствии с их объявленным типом, назывались стиранием типов.

В частности, не требуется использование виртуальных функций или указателей функций для выполнения стирания стилей, если мы используем шаблоны. Пример, уже упомянутый в других ответах, правильного вызова деструктора в соответствии с типом, хранящимся в std::shared_ptr<void>, является примером этого.

Пример, предоставленный в книге Страуструпа, так же приятен.

Подумайте о реализации template<class T> class Vector, контейнера по строкам std::vector. Когда вы будете использовать Vector с большим количеством разных типов указателей, как это часто бывает, компилятор предположительно генерирует другой код для каждого типа указателя.

Этот код вздутия можно предотвратить, указав специализацию указателей Vector для void*, а затем используя эту специализацию как общую базовую реализацию Vector<T*> для всех остальных типов 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)); }
};

Как вы можете видеть, у нас есть строго типизированный контейнер, но Vector<Animal*>, Vector<Dog*>, Vector<Cat*>,..., будут совместно использовать тот же (С++ и) код для реализация, имеющая свой тип указателя, стертый за void*.

Ответ 5

См. серию сообщений для (довольно короткого) списка методов стирания типа и обсуждения компромиссов: Часть I, Часть II, Часть III, Часть IV.

Я еще не видел упомянутого еще Adobe.Poly и Boost.Variant, который в некоторой степени можно считать стиранием типа.

Ответ 6

Как указано Marc, можно использовать cast std::shared_ptr<void>. Например, сохраните тип в указателе функции, произведите его и сохраните в функторе только одного типа:

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