#c #templates #containers
#c #шаблоны #контейнеры
Вопрос:
Предположим, у меня есть структура шаблона:
template<typename T>
struct Entity
{
Entity(int Id) : id(Id)
{
/* init 'data' */
}
T* data;
int id;
};
И затем второй класс, роль которого заключается в хранении объектов различных типов:
typename<Ts...>
class EntityContainer
{
public:
EntityContainer(Ts... Args)
{
/* store 'Args' (which are all template instantiations of entity) in some 'entity container' */
}
template<typename T>
void addNewEntity(Tamp;amp; entity)
{
/* new entities can be added to the 'entity container' at run time */
}
template<typename T>
Tamp; getEntityById(int id)
{
/* stored Entities must be accessible and mutable at run time */
}
private:
/*Entity container goes here e.g. std::tuple<Ts...> */
};
Эти два класса будут использоваться в тандеме, как показано:
//create two entities of differing types
Entity<A> myFirstEntity(1);
Entity<B> mySecondEntity(2);
//store the entities in a container
EntityContainer<Entity<A>, Entity<B>> myContainer(myFirstEntity, mySecondEntity);
//create a new Entity and add it to the container
Entity<C> myThirdEntity(3);
mycontainer.addNewEntity(myThirdEntity);
//retrieve entity from container at run time
myContainer<Entity<B>>.getEntityById(2); //should return 'mySecondEntity'
Итак, я пытался заставить это работать некоторое время, и я довольно близко подошел к использованию an std::tuple
в качестве контейнера сущностей в моем EntityContainer
классе, но проблема в том, что у меня возникли проблемы с доступом и изменением определенных элементов во std::tuple
время выполнения, поэтому я не думаю, что это подходит для работы. Возможно, это вообще неправильный подход, и решение на основе наследования было бы более подходящим, но я не могу придумать ни одного.
Я попытался сделать это как можно более понятным, а также предоставить достаточный код, я надеюсь, что это не требует слишком многого, просто ищу какое-то направление.
Комментарии:
1. Не хочу, чтобы это звучало проще, чем есть на самом деле, но — Когда вы переходите к объяснению реальной проблемы, вы должны оставить для нее место. Прямо сейчас в описании вашей проблемы почти нет места. Внизу тесно… Можете ли вы вместо этого задать вопрос подписи?
2. это похоже на сложную версию системы сущностей-компонентов, вы хотите иметь не шаблонную сущность, обычно просто идентификатор и какой-либо способ определения того, какие компоненты присоединены к этой сущности, тогда у вас есть разные типы компонентов (здесь ваши данные ), которые вы можете присоединить к своей сущности для создания разных типов. Вы хотите, чтобы объект сохранялся в контейнере напрямую, а не через указатель, поэтому при повторении у вас нет косвенности, как в других предложениях
3. Да, но как вы «прикрепляете» компоненты к своей сущности? конечно, в какой-то момент вам понадобится контейнер, принадлежащий сущности, которая содержит компоненты разных типов
4. Компоненты хранятся в другом контейнере, у объекта просто есть индекс в этом контейнере, например
Ответ №1:
решение на основе наследования было бы более подходящим, но я не могу придумать ни одного.
Создайте Entity
подкласс обычного класса и сохраните указатели на базовый класс.
struct EntityBase
{
virtual ~EntityBase() {}
};
template<typename T>
struct Entity : EntityBase
{
...
};
Теперь EntityContainer
можно хранить указатели, предпочтительно интеллектуальные указатели, на EntityBase
.
Ответ №2:
В некотором смысле ваш вопрос похож на «Как я могу хранить std::vector<int>
s и std::string
s и foo
s в одном контейнере?». Разные экземпляры одного и того же шаблона имеют разные типы. Необязательно, чтобы у них было что-то общее (кроме того, что они являются экземплярами одного и того же шаблона, конечно).
Короткий ответ таков: вы не можете. Контейнер может содержать элементы только одного типа.
Однако существуют типы, которые могут принимать значения нескольких типов. Существует std::any
или std::variant
. Ключевое слово для дополнительной информации — «стирание типа».
Возможно, вы знаете стирание типов в форме полиморфизма во время выполнения, который, вероятно, является наиболее простым для понимания воплощением. Вместо сохранения фактического типа в контейнере вы храните указатели (вполне разумные) на базовый класс в контейнере.
Я только обрисую подход:
struct EntityBase {
// declare interface here
virtual ~EntityBase() {}
};
template <typename T>
struct Entity : EntityBase {
T* data;
};
std::vector<std::shared_ptr<EntityBase>> entities;
Простой случай — это когда интерфейс не зависит от T
, т. Е. Вам никогда не нужно приводить или знать, каков динамический тип элементов.
Комментарии:
1. Хорошо, это выглядит многообещающе, проблема возникает, когда у вас есть метод, в
EntityBase
котором требуется доступ кdata
вEntity
, это правильно? Например, метод с сигнатурой:virtual T* getData()
… очевидноT
, что это ничего не значит для этой функции. Есть ли какой-либо способ обойти это?2. @jf192210 Нет, вы бы поместили все, что не зависит от параметра шаблона, в базовый класс и только это. Ознакомьтесь с
std::variant
иstd::visit
затем, к сожалению, я не могу рассказать вам много об этом. Хотя подобные вопросы задают часто, если вы немного поищете, вы должны найти больше ответов3. Нет проблем, вы направили меня в правильном направлении, это все, о чем я мог просить 🙂
4. @jf192210 это выглядит как хорошее введение. Я не читал его, но он кажется довольно исчерпывающим gieseanw.wordpress.com/2017/05/03 /…
5. @jf192210 кстати
std::tuple
— это тип продукта. Набор значений является произведением наборов возможных значений типов, вам скорее нужен так называемый тип суммы. Набор возможных значений типа sum представляет собой объединение всех возможных значений отдельных типов. Надеюсь, это имеет какой-то смысл. Я имею в виду, что astd::tuple<int,double>
содержит строку и double, astd::variant<int,double>
содержитint
илиdouble
, вы хотите последнее
Ответ №3:
Я реализовал нечто подобное в одном из своих недавних проектов. Это самый распространенный способ, с которым я сталкивался до сих пор. Для этого нам нужно иметь 3 базовых объекта,
/**
* This class will be used as the base class of the actual container.
*/
class ContainerBase {
public:
ContainerBase() {}
virtual ~ContainerBase() {}
};
/**
* This class will hold the actual data.
*/
template<class TYPE>
class Container : public ContainerBase {
public:
Container() {}
~Container() {}
std::vector<TYPE> data;
};
/**
* This is the class which is capable of holding all the types of objects with different types.
* It can store multiple objects of the same time since we are using a dynamic array (std::vector<>).
*/
class EntityContainer {
public:
EntityContainer() {}
~EntityContainer() {}
private:
std::vector<std::string> registeredTypes;
std::unordered_map<std::string, ContainerBase*> containerMap;
};
Затем мы должны определить несколько методов для добавления данных. Но для того, чтобы добавить данные в контейнер, мы должны сначала зарегистрировать его.
class EntityContainer {
public:
EntityContainer() {}
~EntityContainer()
{
// Make sure to delete all the allocated memory!
for (auto containerPair : containerMap)
delete containerPair.second;
}
/**
* Check is the required object type is registered by iterating through the registered types.
*/
template<class TYPE>
bool isRegistered()
{
for (autoamp; type : registeredTypes)
if (type == typeid(TYPE).name())
return true;
return false;
}
/**
* Register a new type.
*/
template<class TYPE>
void registerType()
{
// Return if the type is already available.
if (isRegistered<TYPE>())
return;
std::string type = typeid(TYPE).name();
containerMap[type] = new Container<TYPE>;
registeredTypes.push_back(type);
}
/**
* Get the container of the required type.
* This method returns an empty container if the required type is not registered.
*/
template<class TYPE>
Container<TYPE>* getContainer()
{
if (!isRegistered<TYPE>())
registerType<TYPE>();
return dynamic_cast<Container<TYPE>*>(containerMap[typeid(TYPE).name()]);
}
/**
* Add a new entity to the TYPE container.
*/
template<class TYPE>
void addNewEntity(TYPEamp;amp; data)
{
getContainer<TYPE>()->data.push_back(std::move(data));
}
/**
* Get a data which is stored in the container of the required type using its ID.
*/
template<class TYPE>
TYPE getEntityByID(int ID)
{
std::vector<TYPE> tempVec = getContainer<TYPE>()->data;
for (auto entity : tempVec)
if (entity.id == ID)
return entity;
return TYPE();
}
getContainer<TYPE>()->data.push_back(data);
}
/**
* Get a data which is stored in the container of the required type using its index.
*/
template<class TYPE>
TYPE getData(size_t index)
{
return getContainer<TYPE>()->data[index];
}
private:
std::vector<std::string> registeredTypes;
std::unordered_map<std::string, ContainerBase*> containerMap;
};
Теперь, используя приведенную выше структуру данных, вы можете выполнить свою задачу.
//store the entities in a container
EntityContainer container;
Entity<A> myFirstEntity(1);
Entity<B> mySecondEntity(2);
Entity<C> myThirdEntity(3);
mycontainer.addNewEntity(myFirstEntity);
mycontainer.addNewEntity(mySecondEntity);
mycontainer.addNewEntity(myThirdEntity);
mycontainer.getEntityByID<Entity<C>>(3);
Эта структура может содержать объекты практически любого типа. Единственным недостатком здесь является то, что вам нужно знать тип во время компиляции. Но все же есть множество способов расширить эту структуру в соответствии с вашими потребностями.
Комментарии:
1. Вы дали мне много материала для изучения, спасибо 🙂 Это выглядит очень многообещающе
2. @jf192210 в любое время 😉
Ответ №4:
Гетерогенная реализация контейнера:
#include <vector>
#include <unordered_map>
#include <functional>
#include <iostream>
#include<experimental/type_traits>
// ### heterogeneous container ###
namespace container
{
template<class...>
struct type_list{};
template<class... TYPES>
struct visitor_base
{
using types = container::type_list<TYPES...>;
};
struct entity_container
{
public:
entity_container() = default;
template<typename ...Ts>
entity_container(Ts ...args)
{
auto loop = [amp;](autoamp; arg)
{
this->push_back(arg);
};
(loop(args), ...);
}
entity_container(const entity_containeramp; _other)
{
*this = _other;
}
entity_containeramp; operator=(const entity_containeramp; _other)
{
clear();
clear_functions = _other.clear_functions;
copy_functions = _other.copy_functions;
size_functions = _other.size_functions;
stored_ids_functions = _other.stored_ids_functions;
id_delete_functions = _other.id_delete_functions;
for (autoamp;amp; copy_function : copy_functions)
{
copy_function(_other, *this);
}
return *this;
}
template<class T>
void push_back(const Tamp; _t)
{
//is a new container being made? if so ...
if (items<T>.find(this) == std::end(items<T>))
{
clear_functions.emplace_back([](entity_containeramp; _c){items<T>.erase(amp;_c);});
id_delete_functions.emplace_back([amp;](entity_containeramp; _c, int id)
{
for(autoamp; ent : items<T>[amp;_c])
{
if(ent.getId() == id)
items<T>[amp;_c].erase( std::find(items<T>[amp;_c].begin(), items<T>[amp;_c].end(), ent) );
}
});
copy_functions.emplace_back([](const entity_containeramp; _from, entity_containeramp; _to)
{
items<T>[amp;_to] = items<T>[amp;_from];
});
size_functions.emplace_back([](const entity_containeramp; _c){return items<T>[amp;_c].size();}); //returns the size of the vector
stored_ids_functions.emplace_back([](const entity_containeramp; _c)
{
std::vector<int> stored_ids;
for(autoamp; ent : items<T>[amp;_c])
stored_ids.push_back(ent.getId());
return stored_ids;
});
}
items<T>[this].push_back(_t);
}
void clear()
{
for (autoamp;amp; clear_func : clear_functions)
{
clear_func(*this);
}
}
template<class T>
size_t number_of() const
{
auto iter = items<T>.find(this);
if (iter != items<T>.cend())
return items<T>[this].size();
return 0;
}
size_t size() const
{
size_t sum = 0;
for (autoamp;amp; size_func : size_functions)
sum = size_func(*this);
// gotta be careful about this overflowing
return sum;
}
bool exists_by_id(int id) const
{
for(autoamp;amp; id_func : stored_ids_functions) //visit each of the id functions
{
std::vector<int> ids = id_func(*this);
auto res = std::find(std::begin(ids), std::end(ids), id);
if(res != std::end(ids))
return true;
}
return false;
}
void delete_by_id(int id)
{
for(autoamp;amp; id_delete_func : id_delete_functions)
id_delete_func(*this, id);
}
void print_stored_ids()
{
for(autoamp;amp; id_func : stored_ids_functions) //visit each of the id functions
{
std::vector<int> ids = id_func(*this);
for(autoamp; id : ids)
std::cout << "id: " << id << std::endl;
}
}
~entity_container()
{
clear();
}
template<class T>
void visit(Tamp;amp; visitor)
{
visit_impl(visitor, typename std::decay_t<T>::types{});
}
private:
template<class T>
static std::unordered_map<const entity_container*, std::vector<T>> items;
template<class T, class U>
using visit_function = decltype(std::declval<T>().operator()(std::declval<Uamp;>()));
template<class T, class U>
static constexpr bool has_visit_v = std::experimental::is_detected<visit_function, T, U>::value;
template<class T, template<class...> class TLIST, class... TYPES>
void visit_impl(Tamp;amp; visitor, TLIST<TYPES...>)
{
(..., visit_impl_help<std::decay_t<T>, TYPES>(visitor));
}
template<class T, class U>
void visit_impl_help(Tamp; visitor)
{
static_assert(has_visit_v<T, U>, "Visitors must provide a visit function accepting a reference for each type");
for (autoamp;amp; element : items<U>[this])
{
visitor(element);
}
}
std::vector<std::function<void(entity_containeramp;)>> clear_functions;
std::vector<std::function<void(const entity_containeramp;, entity_containeramp;)>> copy_functions;
std::vector<std::function<size_t(const entity_containeramp;)>> size_functions;
std::vector<std::function<std::vector<int>(const entity_containeramp;)>> stored_ids_functions;
std::vector<std::function<void(entity_containeramp;, int)>> id_delete_functions;
};
template<class T>
std::unordered_map<const entity_container*, std::vector<T>> entity_container::items;
}
Использование:
template<class T>
struct Entity
{
public:
Entity(int Id) : id(Id)
{
data = new T();
}
bool operator==(const Entity<T>amp; rhs) const {
return (this->id == rhs.id amp;amp; this->data == rhs.data);
}
int getId(){return id;}
T*amp; getData(){ return data; }
private:
int id;
T* data;
};
struct A{};
struct B{};
struct C{};
/* ## main ## */
int main()
{
Entity<A> entity_a(1);
Entity<B> entity_b(2);
Entity<C> entity_c(3);
Entity<C> entity_d(10);
container::entity_container c(entity_a, entity_b, entity_c);
c.push_back(entity_d);
c.print_stored_ids(); //prints the id's of all store entities
c.delete_by_id(3); //deletes the store entities with the parsed id
c.delete_by_id(1);
c.delete_by_id(10);
c.print_stored_ids();
}
В основном получено изhttps://gieseanw.wordpress.com/2017/05/03/a-true-heterogeneous-container-in-c / с некоторыми личными дополнениями для адаптации контейнера к типу, такому как Entity
(т. Е. с идентификатором и указателем на некоторые данные). Может быть кому-то полезно.