(Под стиранием типов я имею в виду скрытие некоторой или всей информации о типах, относящейся к классу, что-то вроде 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();
}
источник
shared_ptr
не отражает этого, он всегда будет таким же,shared_ptr<int>
например, в отличие от стандартного контейнера.As
функция не будет реализована таким образом. Как я уже сказал, ни в коем случае не безопасно для использования! :)function
,shared_ptr
,any
И т.д.? Все они используют стирание типа для удобства пользователя.Ответы:
Все методы стирания типов в C ++ выполняются с помощью указателей на функции (для поведения) и
void*
(для данных). «Разные» методы просто отличаются тем, как они добавляют семантический сахар. Виртуальные функции, например, просто семантический сахар дляiow: функциональные указатели.
Тем не менее, есть одна техника, которая мне особенно нравится: она
shared_ptr<void>
просто поражает воображение людей, которые не знают, что вы можете сделать это: вы можете хранить любые данные в ashared_ptr<void>
, и при этом вызывать правильный деструктор в конец, потому чтоshared_ptr
конструктор является шаблоном функции и по умолчанию будет использовать тип фактического объекта, переданного для создания средства удаления:Конечно, это обычное
void*
стирание типа / указатель на функцию, но очень удобно упаковано.источник
shared_ptr<void>
моего друга на примере реализации всего несколько дней назад. :) Это действительно круто.unique_ptr
не стирает тип с помощью средства удаления, поэтому, если вы хотите назначить aunique_ptr<T>
для aunique_ptr<void>
, вам нужно явно указать аргумент для удаления, который знает, как удалитьT
объект через avoid*
. Если вы теперь хотите назначитьS
тоже, то вам нужен явный удалитель, который знает, как удалитьT
сквозной элемент a,void*
а такжеS
сквозной элемент avoid*
, и , учитывая avoid*
, знает, является ли онT
илиS
. В этот момент вы написали удаленный тип для удаленияunique_ptr
, а затем он также работает дляunique_ptr
. Просто не из коробки.unique_ptr
?» Полезно для некоторых людей, но не ответил на мой вопрос. Я предполагаю, что ответ заключается в том, что общие указатели получили больше внимания при разработке стандартной библиотеки. Что, на мой взгляд, немного грустно, потому что уникальные указатели проще, поэтому проще реализовать основные функции, и они более эффективны, поэтому люди должны их больше использовать. Вместо этого у нас есть полная противоположность.По сути, это ваши варианты: виртуальные функции или указатели функций.
То, как вы храните данные и связываете их с функциями, может варьироваться. Например, вы можете хранить указатель на базу и иметь производный класс, содержащий данные и реализации виртуальных функций, или вы можете хранить данные в другом месте (например, в отдельно выделенном буфере) и просто иметь производный класс, предоставляющий реализации виртуальных функций, которые принимают,
void*
что указывает на данные. Если вы храните данные в отдельном буфере, вы можете использовать указатели функций, а не виртуальные функции.Хранение указателя на базу хорошо работает в этом контексте, даже если данные хранятся отдельно, если есть несколько операций, которые вы хотите применить к стертым данным. В противном случае вы получите несколько указателей на функции (по одному для каждой из стертых по типу функций) или функции с параметром, который задает выполняемую операцию.
источник
Я хотел бы также рассмотреть ( по аналогии с
void*
) использование «сырого хранения»:char buffer[N]
.В C ++ 0x у вас есть
std::aligned_storage<Size,Align>::type
для этого.Вы можете хранить там все, что захотите, при условии, что он достаточно мал, и вы правильно справитесь с выравниванием.
источник
std::aligned_storage
хотя, спасибо! :)std::aligned_storage<...>::type
это просто необработанный буфер, который, в отличие от этогоchar [sizeof(T)]
, соответствующим образом выровнен. Однако сам по себе он инертен: он не инициализирует свою память, не создает объект, ничего. Поэтому, если у вас есть буфер этого типа, вы должны вручную создавать объекты внутри него (либо с помощью размещения,new
либо с помощьюconstruct
метода распределителя ), и вам также необходимо вручную уничтожать объекты внутри него (либо вручную вызывая их деструктор, либо используяdestroy
метод распределителя ). ).Страуструп, в языке программирования C ++ (4-е издание) §25.3 , утверждает:
В частности, не требуется использование виртуальных функций или указателей на функции для удаления типов, если мы используем шаблоны.
std::shared_ptr<void>
Примером этого является уже упомянутый в других ответах случай правильного вызова деструктора в соответствии с типом, хранящимся в .Пример, приведенный в книге Страуструпа, столь же приятен.
Подумайте о реализации
template<class T> class Vector
, контейнер по линииstd::vector
. Когда вы будете использовать вашVector
с множеством различных типов указателей, как это часто бывает, компилятор предположительно будет генерировать различный код для каждого типа указателя.Это раздувание кода можно предотвратить, определив специализацию Vector для
void*
указателей, а затем используя эту специализацию в качестве общей базовой реализацииVector<T*>
для всех других типовT
:Как вы можете видеть, мы имеем строго типизированный контейнер , но
Vector<Animal*>
,Vector<Dog*>
,Vector<Cat*>
..., будут одни и те же (C ++ и код для реализации бинарного), имея их тип указателя стерта заvoid*
.источник
template<typename Derived> VectorBase<Derived>
который затем специализируется какtemplate<typename T> VectorBase<Vector<T*> >
. Более того, этот подход работает не только для указателей, но и для любого типа.См. Эту серию постов для (довольно короткого) списка методов стирания типов и обсуждения компромиссов: Часть I , Часть II , Часть III , Часть IV .
Я еще не упомянул о Adobe.Poly и Boost.Variant , которые в некоторой степени можно считать стиранием типов.
источник
Как утверждает Марк, можно использовать приведение
std::shared_ptr<void>
. Например, сохраните тип в указателе функции, приведите его и сохраните в функторе только одного типа:источник