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


136

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

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

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

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

Редактировать
(Поскольку я не был уверен, добавим ли это в качестве ответа или просто отредактирую вопрос, я просто сделаю более безопасный.)
Еще одна хорошая техника, позволяющая скрыть фактический тип чего-либо без виртуальных функций или void*перебора, - здесь работает один 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
Под «стиранием типа» вы действительно имеете в виду «полиморфизм»? Я думаю, что «стирание типа» имеет определенное значение, которое обычно ассоциируется, например, с дженериками Java.
Оливер Чарльзуорт

3
@Oli: Стирание типа может быть реализовано с полиморфизмом, но это не единственный вариант, мой второй пример показывает это. :) А с стиранием типа я просто имею в виду, что ваша структура не зависит, например, от типа шаблона. Boost.Function не заботится, передаете ли вы ему функтор, указатель на функцию или даже лямбду. То же самое с Boost.Shared_Ptr. Вы можете указать функцию выделения и освобождения, но фактический тип объекта shared_ptrне отражает этого, он всегда будет таким же, shared_ptr<int>например, в отличие от стандартного контейнера.
Xeo

2
@Matthieu: я считаю, что второй пример также безопасен. Вы всегда знаете точный тип, на котором работаете. Или я что-то упустил?
Xeo

2
@Matthieu: Ты прав. Обычно такая Asфункция не будет реализована таким образом. Как я уже сказал, ни в коем случае не безопасно для использования! :)
Xeo

4
@ lurscher: Ну ... никогда не использовал ни одну из следующих версий boost или std ? function, shared_ptr, anyИ т.д.? Все они используют стирание типа для удобства пользователя.
Xeo

Ответы:


100

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

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

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

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

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

Конечно, это обычное void*стирание типа / указатель на функцию, но очень удобно упаковано.


9
По случайному совпадению мне пришлось объяснить поведение shared_ptr<void>моего друга на примере реализации всего несколько дней назад. :) Это действительно круто.
Xeo

Хороший ответ; чтобы сделать это удивительным, набросок того, как можно создать статически vake-таблицу для каждого стертого типа, очень полезен. Обратите внимание, что реализации fake-vtables и указателей на функции дают вам известные структуры с размером памяти (по сравнению с чисто виртуальными типами), которые можно легко хранить локально и (легко) отделять от данных, которые они виртуализируют.
Якк - Адам Невраумонт

Итак, если shared_ptr затем сохраняет Derived *, но Base * не объявляет деструктор как виртуальный, shared_ptr <void> по-прежнему работает, как предполагалось, так как он даже не знал о базовом классе с самого начала. Прохладно!
TamaMcGlinn

@Apollys: он делает, но unique_ptrне стирает тип с помощью средства удаления, поэтому, если вы хотите назначить a unique_ptr<T>для a unique_ptr<void>, вам нужно явно указать аргумент для удаления, который знает, как удалить Tобъект через a void*. Если вы теперь хотите назначить Sтоже, то вам нужен явный удалитель, который знает, как удалить Tсквозной элемент a, void*а также Sсквозной элемент a void*, и , учитывая a void*, знает, является ли он Tили S. В этот момент вы написали удаленный тип для удаления unique_ptr, а затем он также работает для unique_ptr. Просто не из коробки.
Марк Мутц - Ммуц

Я чувствую, что вы ответили на вопрос «Как мне обойти тот факт, что это не работает unique_ptr?» Полезно для некоторых людей, но не ответил на мой вопрос. Я предполагаю, что ответ заключается в том, что общие указатели получили больше внимания при разработке стандартной библиотеки. Что, на мой взгляд, немного грустно, потому что уникальные указатели проще, поэтому проще реализовать основные функции, и они более эффективны, поэтому люди должны их больше использовать. Вместо этого у нас есть полная противоположность.
Apollys поддерживает Монику

54

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

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

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


1
Итак, другими словами, примеры, которые я привел в вопросе? Тем не менее, спасибо за то, что написали это, особенно в отношении виртуальных функций и множества операций над стертыми данными.
Xeo

Есть как минимум 2 других варианта. Я сочиняю ответ.
Джон Диблинг

25

Я хотел бы также рассмотреть ( по аналогии с void*) использование «сырого хранения»: char buffer[N].

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

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


4
Ну да, Boost.Function на самом деле использует комбинацию этого и второго примера, который я привел. Если функтор достаточно мал, он хранит его внутри внутри functor_buffer. Полезно знать, std::aligned_storageхотя, спасибо! :)
Xeo

Вы также можете использовать новое размещение для этого.
rustyx

2
@RustyX: На самом деле, вы должны . std::aligned_storage<...>::typeэто просто необработанный буфер, который, в отличие от этого char [sizeof(T)], соответствующим образом выровнен. Однако сам по себе он инертен: он не инициализирует свою память, не создает объект, ничего. Поэтому, если у вас есть буфер этого типа, вы должны вручную создавать объекты внутри него (либо с помощью размещения, newлибо с помощью constructметода распределителя ), и вам также необходимо вручную уничтожать объекты внутри него (либо вручную вызывая их деструктор, либо используя destroyметод распределителя ). ).
Матье М.

22

Страуструп, в языке программирования C ++ (4-е издание) §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*>..., будут одни и те же (C ++ и код для реализации бинарного), имея их тип указателя стерта за void*.


2
Не имея смысла быть богохульным: я бы предпочел CRTP технике, данной Страуструпом.
Давидхай

@davidhigh Что ты имеешь в виду?
Паоло М

Можно получить то же поведение (с менее понятным синтаксисом), используя базовый класс CRTP,template<typename Derived> VectorBase<Derived> который затем специализируется как template<typename T> VectorBase<Vector<T*> >. Более того, этот подход работает не только для указателей, но и для любого типа.
Дэвидхай

3
Обратите внимание, что хорошие компоновщики C ++ объединяют идентичные методы и функции: золотой компоновщик или MSVC-компиляция. Код генерируется, но затем отбрасывается при связывании.
Якк - Адам Невраумонт

1
@ davidhigh Я пытаюсь понять ваш комментарий и задаюсь вопросом, можете ли вы дать мне ссылку или название шаблона для поиска (не CRTP, а название метода, который позволяет стирать типы без виртуальных функций или указателей на функции) , С уважением, - Крис
Крис Чиассон


7

Как утверждает Марк, можно использовать приведение 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
    // !
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.