Шаблон С++: один список по классу, как разложить код?

Предположим, что у меня есть этот класс:

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);
}