Шаблон С++: один список по классу, как разложить код?
Предположим, что у меня есть этот класс:
class Component1;
class Component2;
// many different Components
class Component42;
class MyClass
{
public:
MyClass(void) {};
std::list<Component1> component1List;
std::list<Component2> component2List;
// one list by component
std::list<Component42> component42List;
};
Я хотел бы создать функцию со следующей сигнатурой:
template<class T> void addElement(T component);
Он должен сделать следующее:
- Если
component
имеет тип Component1
, добавьте его в Component1List
- Если
component
имеет тип Component2
, добавьте его в Component2List
и т.д.
Возможно ли это? Какой хороший способ сделать это?
Я могу получить одно и то же поведение с такой функцией, как:
template<class T> void addElement(int componentType, T component);
но я не хочу указывать componentType
следующим образом: это бесполезная информация, и она открывает дверь для возможных ошибок (если componentType
не представляет тип компонента).
Ответы
Ответ 1
std::tuple
для спасения.
изменений:
-
использовать std::decay_t
-
добавлена форма вариационного аргумента
-
add_component()
теперь возвращает ссылку на это, чтобы разрешить цепочку вызовов.
#include <iostream>
#include <list>
#include <utility>
#include <type_traits>
#include <tuple>
class Component1 {};
class Component2 {};
struct Component3 {
Component3() {}
};
// many different Components
template<class...ComponentTypes>
class MyClassImpl
{
template<class Component> using list_of = std::list<Component>;
public:
using all_lists_type =
std::tuple<
list_of<ComponentTypes> ...
>;
// add a single component
template<class Component>
MyClassImpl& add_component(Component&& c)
{
list_for<Component>().push_back(std::forward<Component>(c));
return *this;
}
// add any number of components
template<class...Components>
MyClassImpl& add_components(Components&&... c)
{
using expand = int[];
void(expand { 0, (void(add_component(std::forward<Components>(c))), 0)... });
return *this;
}
template<class Component>
auto& list_for()
{
using component_type = std::decay_t<Component>;
return std::get<list_of<component_type>>(_lists);
}
template<class Component>
const auto& list_for() const
{
using component_type = std::decay_t<Component>;
return std::get<list_of<component_type>>(_lists);
}
private:
all_lists_type _lists;
};
using MyClass = MyClassImpl<Component1, Component2, Component3>;
int main()
{
MyClass c;
c.add_component(Component1());
c.add_component(Component2());
const Component3 c3;
c.add_component(c3);
c.add_components(Component1(),
Component2(),
Component3()).add_components(Component3()).add_components(Component1(),
Component2());
std::cout << c.list_for<Component1>().size() << std::endl;
return 0;
}
Ответ 2
Самый простой вариант - просто не использовать шаблоны, а перегрузить функцию addElement()
:
void addElement(Component1 element)
{
this->element1List.push_back(element);
}
void addElement(Component2 element)
{
this->element2List.push_back(element);
}
// ... etc
Однако это может стать утомительным, если у вас их много (и у вас не просто addElement()
, я думаю). Использование макроса для генерации кода для каждого типа может все еще выполнять работу с разумными усилиями.
Если вы действительно хотите использовать шаблоны, вы можете использовать функцию шаблона и специализировать функцию шаблона для каждого типа. Тем не менее, это не уменьшает количество повторений кода по сравнению с вышеуказанным подходом. Кроме того, вы все равно можете уменьшить его с помощью макросов для генерации кода.
Однако есть надежда на это в общем виде. Во-первых, создайте тип, содержащий список:
template<typename T>
struct ComponentContainer
{
list<T> componentList;
};
Теперь производный класс просто наследуется от этого класса и использует систему типа С++ для определения правильного базового класса контейнера:
class MyClass:
ComponentContainer<Component1>,
ComponentContainer<Component2>,
ComponentContainer<Component3>
{
public:
template<typename T>
void addElement(T value)
{
ComponentContainer<T>& container = *this;
container.componentList.push_back(value);
}
}
Примечания здесь:
- Это использует частное наследование, которое очень похоже на сдерживание, которое вы изначально использовали.
- Даже если
ComponentContainer
является базовым классом, он не имеет виртуальных функций и даже не виртуального деструктора. Да, это опасно и должно быть четко документировано. Я бы не добавил виртуального деструктора, хотя из-за накладных расходов, которые он имеет, и потому, что он не нужен.
- Вы можете полностью удалить промежуточный контейнер и получить также от
list<T>
. Я не сделал этого, потому что он сделает все функции list
memberfunctions доступными в классе MyClass
(даже если это не публично), что может ввести в заблуждение.
- Вы не можете поместить функцию
addElement()
в шаблон базового класса, чтобы избежать шаблона в производном классе. Простая причина заключается в том, что различные базовые очки сканируются для функции addElement()
, и только тогда выполняется разрешение перегрузки. Компилятор только найдет addElement()
в первом базовом классе.
- Это простое решение на С++ 98, для С++ 11 я бы посмотрел на поисковые решения, основанные на типе, предложенные Йенсом и Ричардом.
Ответ 3
Если не слишком много классов, вы можете перегружать. Решение на основе шаблонов может быть выполнено с помощью поиска по типу для кортежей:
class MyClass {
public:
template<typename T> void addElement(T&& x) {
auto& l = std::get<std::list<T>>(lists);
l.insert( std::forward<T>(x) );
}
private:
std::tuple< std::list<Component1>, std::list<Component2> > lists;
};
Ответ 4
Если вы не знаете заранее, какие типы вам нужно хранить при создании экземпляра multi-container, нужно скрыть типы и с помощью type_index
сохранить карту списков:
struct Container {
struct Entry {
void *list;
std::function<void *(void*)> copier;
std::function<void(void *)> deleter;
};
std::map<std::type_index, Entry> entries;
template<typename T>
std::list<T>& list() {
Entry& e = entries[std::type_index(typeid(T))];
if (!e.list) {
e.list = new std::list<T>;
e.deleter = [](void *list){ delete ((std::list<T> *)list); };
e.copier = [](void *list){ return new std::list<T>(*((std::list<T> *)list)); };
}
return *((std::list<T> *)e.list);
}
~Container() {
for (auto& i : entries) i.second.deleter(i.second.list);
}
Container(const Container& other) {
// Not exception safe... se note
for (auto& i : other.entries) {
entries[i.first] = { i.second.copier(i.second.list),
i.second.copier,
i.second.deleter };
}
};
void swap(Container& other) { std::swap(entries, other.entries); }
Container& operator=(const Container& other) {
Container(other).swap(*this);
return *this;
};
Container() { }
};
который может использоваться как:
Container c;
c.list<int>().push_back(10);
c.list<int>().push_back(20);
c.list<double>().push_back(3.14);
ПРИМЕЧАНИЕ. Конструктор копирования, как написано сейчас, не является безопасным, поскольку в случае, если a copier
выбрасывает (из-за нехватки памяти или из-за того, что конструктор копирования элемента внутри списка выбрасывает), уже выделенные списки не будут перераспределена.
Ответ 5
void addElement(Component1 component) {
componentList1.insert(component);
}
void addElement(Component2 component) {
componentList2.insert(component);
}