Введіть методи стирання


136

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

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

Тоді з'являється можливість з покажчиками функцій на шаблонні функції, утримуючи фактичний об'єкт у 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>наприклад, на відміну від стандартного контейнера.
Ксео

2
@Matthieu: Я вважаю другий приклад також безпечним. Ви завжди знаєте точний тип, на якому працюєте. Або я щось пропускаю?
Ксео

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>просто тому, що вона збиває з розуму людей, які не знають, що ти можеш це зробити. Ви можете зберігати будь-які дані в shared_ptr<void>, і все ще мати правильний деструктор, викликаний у наприкінці, тому що shared_ptrконструктор є шаблоном функції і буде використовувати тип фактичного об'єкта, переданого для створення делетера за замовчуванням:

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

Звичайно, це просто звичайне void*стирання / функціонально-вказівний тип стирання, але дуже зручно упаковано.


9
Випадково мені довелося пояснити поведінку shared_ptr<void>мого друга на прикладі реалізації лише кілька днів тому. :) Це дійсно круто.
Xeo

Хороша відповідь; щоб зробити це дивовижним, ескіз того, як може бути статично створений фальшивий vtable для кожного стертого типу, є дуже навчальним. Зауважте, що підроблені vtables та реалізація функціональних вказівників дають вам відомі структури розміру пам'яті (порівняно з чисто-віртуальними типами), які можна легко зберігати локально та (легко) відлучати від даних, які вони віртуалізують.
Якк - Адам Невраумон

тому, якщо shared_ptr зберігає похідне *, але Base * не оголосив деструктор як віртуальний, shared_ptr <void> все ще працює за призначенням, оскільки він ніколи навіть не знав про базовий клас для початку. Класно!
TamaMcGlinn

@Apollys: Це робить, але unique_ptrне видаляє делетер , тому якщо ви хочете призначити unique_ptr<T>a unique_ptr<void>, вам потрібно надати аргумент делетера, явно, який знає, як видалити Tчерез a void*. Якщо ви тепер також хочете призначити Sантену, вам потрібен делетер, який чітко знає, як видалити Tчерез a, void*а також Sчерез a void*, і , даючи a void*, знає, чи це a, Tабо an S. У цей момент ви написали стираний видалювач для unique_ptr, а потім він також працює для unique_ptr. Тільки не з коробки.
Марк Муц - mmutz

Мені здається, що питання, на яке ви відповіли, було: "Як мені вирішити той факт, що це не працює unique_ptr?" Корисно для деяких людей, але не звернулося до мого питання. Я здогадуюсь відповіді, тому що спільні покажчики отримали більше уваги при розробці стандартної бібліотеки. Я думаю, що це сумно, тому що унікальні покажчики простіші, тому їм слід легше реалізовувати основні функціональні можливості, і вони більш ефективні, тому люди повинні їх більше використовувати. Натомість у нас все навпаки.
Аполліс підтримує Моніку

54

По суті, це ваші варіанти: віртуальні функції або покажчики функцій.

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

Збереження вказівника на базу добре працює в цьому контексті, навіть якщо дані зберігаються окремо, якщо є кілька операцій, які ви хочете застосувати до даних, що стираються. В іншому випадку ви маєте кілька покажчиків функцій (по одному для кожної стираної функції) або функцій з параметром, який визначає операцію, яку потрібно виконати.


1
Отже, іншими словами приклади, які я наводив у запитанні? Хоча, дякую, що написав це так, особливо wrt до віртуальних функцій та декількох операцій над видаленими на тип даних.
Xeo

Є як мінімум 2 інші варіанти. Я складаю відповідь.
Джон Дайблінг

25

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

У C ++ 0x у вас є std::aligned_storage<Size,Align>::typeдля цього.

Ви можете зберігати все, що завгодно, до тих пір, поки воно досить мало і ви правильно займаєтеся вирівнюванням.


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

Для цього також можна використовувати нове розташування .
rustyx

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

22

Страуструп в C ++ Мова програмування (4 - е видання) §25.3 , говорить:

Варіанти методики використання єдиного представлення часу виконання для значень декількох типів і спираючись на (статичну) систему типів, щоб переконатися, що вони використовуються лише відповідно до їх заявленого типу, називали стиранням типу .

Зокрема, не потрібно використовувати віртуальних функцій чи покажчиків функцій для виконання стирання типу, якщо ми використовуємо шаблони. Приклад цього, вже згадуваний в інших відповідях, правильного виклику деструктора відповідно до типу, що зберігається у a, 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 техніці, яку надав Stroustrup.
Давидхіг

@davidhigh Що ти маєш на увазі?
Паоло М

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

3
Зауважте, що хороші C ++-лінкери об'єднують ідентичні методи та функції: золотий лінкер або MSVC comdat fold. Код формується, але потім відкидається під час посилання.
Якк - Адам Невраумон

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.