Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ Template : one list by class, how to factorize the code?

Suppose I have this class :

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

I would like to create a function with the following signature:

template<class T> void addElement(T component);

It should do the following:

  • if component is of type Component1, add it to Component1List
  • if component is of type Component2, add it to Component2List, etc.

Is it possible? What's a good way to do this?

I can obtain the same behaviour with a function like :

template<class T> void addElement(int componentType, T component);

but I'd rather not have to specify the componentType like this : it's useless information and it open the door to possible errors (if componentType doesn't represent the type of component).

like image 363
Tryss Avatar asked Jan 04 '16 08:01

Tryss


4 Answers

std::tuple to the rescue.

changelog:

  • use std::decay_t

  • added the variadic argument form

  • add_component() now returns a reference to this to allow call-chaining.


#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;
}
like image 101
Richard Hodges Avatar answered Nov 15 '22 14:11

Richard Hodges


The most straightforward variant is to simply not use templates but to overload the addElement() function:

void addElement(Component1 element)
{
    this->element1List.push_back(element);
}
void addElement(Component2 element)
{
    this->element2List.push_back(element);
}
// ... etc

However, this might get tedious if you have many of these (and you don't just have addElement(), I guess). Using a macro to generate the code for each type could still do the job with reasonable effort.

If you really want to use templates, you could use a template function and specialize the template function for each type. Still, this doesn't reduce the amount of code repetition when compared with the above approach. Also, you could still reduce it using macros to generate the code.

However, there's hope for doing this in a generic way. Firstly, let's create a type that holds the list:

template<typename T>
struct ComponentContainer
{
    list<T> componentList;
};

Now, the derived class just inherits from this class and uses C++ type system to locate the correct container baseclass:

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

Notes here:

  • This uses private inheritance, which is very similar to the containment you originally used.
  • Even though ComponentContainer is a baseclass, it doesn't have any virtual functions and not even a virtual destructor. Yes, this is dangerous and should be documented clearly. I wouldn't add a virtual destructor though, because of the overhead it has and because it shouldn't be needed.
  • You could drop the intermediate container altogether and derive from list<T>, too. I didn't because it will make all of list's memberfunctions available in class MyClass (even if not publicly), which might be confusing.
  • You can't put the addElement() function into the base class template to avoid the template in the derived class. The simple reason is that the different baseclasses are scanned in order for a addElement() function and only then overload resolution is performed. The compiler will only find the addElement() in the first baseclass therefore.
  • This is a plain C++98 solution, for C++11 I'd look at the type-based tuple lookup solutions suggested by Jens and Richard.
like image 25
Ulrich Eckhardt Avatar answered Nov 15 '22 13:11

Ulrich Eckhardt


If there are not too many classes you could go with overloading. A template-based solution could be done with type-based lookup for tuples:

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;
};
like image 2
Jens Avatar answered Nov 15 '22 12:11

Jens


If you don't know in advance the types you will need storing when instantiating the multi-container an option is to hide the types and using type_index to keep a map of lists:

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

that can be used as:

Container c;
c.list<int>().push_back(10);
c.list<int>().push_back(20);
c.list<double>().push_back(3.14);

NOTE: the copy constructor as written now is not exception safe because in case a copier throws (because of an out of memory or because a copy constructor of an element inside a list throws) the already allocated lists will not be deallocated.

like image 1
6502 Avatar answered Nov 15 '22 12:11

6502