14

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).

DCTLib
  • 1,016
  • 8
  • 22
Tryss
  • 303
  • 2
  • 8
  • Related to [vector-template-c-class-adding-to-vector](http://stackoverflow.com/questions/20971819/vector-template-c-class-adding-to-vector) – Jarod42 Jan 04 '16 at 10:59

5 Answers5

13

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;
}
Community
  • 1
  • 1
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • You should remove reference and constness/volatileness from the deduced `Component` inside `list_of` – Piotr Skotnicki Jan 04 '16 at 09:04
  • 1
    `std::decay_t` may replace `std::remove_cv_t>` – Jarod42 Jan 04 '16 at 10:45
  • @Tryss updated in response to comments. Added call chaining and ability to add multiple components at once, in case that improves clarity at the call site. – Richard Hodges Jan 04 '16 at 11:13
  • @RichardHodges : your answer was really helpfull – Tryss Jan 04 '16 at 12:18
  • @PiotrSkotnicki Thanks for the edit. apple's libc++ is really permissive WRT `#include` of standard headers. Catches me out every time I recompile on linux :-) – Richard Hodges Jan 04 '16 at 12:49
  • @RichardHodges Can you explain how `void(expand { 0, (void(add_component(std::forward(c))), 0)... });` works for myself and other curious readers? C++11 is hard :) – Agop Jan 04 '16 at 19:45
  • @RichardHodges I think I got it... It's [pack expansion within a braced initializer list](http://en.cppreference.com/w/cpp/language/parameter_pack#Braced_init_lists), like: `int dummy[sizeof...(Components)] = { (add_component(std::forward(c)), 0)... };`. That results in an array of `0`s, but `add_component()` is called for each component as a side effect. – Agop Jan 04 '16 at 19:58
  • 1
    @Agop. Sure. expand is a typedef of an array of ints. This array is expanded by unfolding the variadic argument pack. The first element of the array is 0, and each subsequent value is initialised with (void(add_component(...)), 0). You will remember that the comma operator is a sequence point in c++ so it will force the execution of add_component(), then discard the result and then evaluate the 0, which goes in the array. Finally, the entire array is thrown away so only the side-effects remain. – Richard Hodges Jan 04 '16 at 19:59
  • 1
    @Agop yes that's it, but remember that zero-sized arrays are illegal in c++ (legal in c) so using the initial zero protects the array from being size zero if the argument pack is empty. – Richard Hodges Jan 04 '16 at 20:00
  • @RichardHodges Got it, thanks! I didn't think about the argument pack being empty. What about the `void`s, I assume those are there to prevent the compiler from complaining about unused variables? – Agop Jan 04 '16 at 20:03
  • 1
    @Agop indeed they are. – Richard Hodges Jan 04 '16 at 20:06
11

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.
Ulrich Eckhardt
  • 16,572
  • 3
  • 28
  • 55
  • Thanks for this interesting solution. I use C++11, so I'll use the tuple solution, but your help was appreciated :) – Tryss Jan 04 '16 at 09:17
2

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;
};
Ulrich Eckhardt
  • 16,572
  • 3
  • 28
  • 55
Jens
  • 9,058
  • 2
  • 26
  • 43
1

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.

6502
  • 112,025
  • 15
  • 165
  • 265
  • I'm pretty sure you could replace every C-style cast there with a proper C++ cast, which preserves some checks by the compiler. Further, you could use a simple `map>` to store a pointer to the according `list` (maybe even `unique_ptr`, though I'm not sure). This doesn't solve the copying (if that is needed at all), though, for which the clone function is needed or maybe Boost.Any. I still do like that solution, although it incurs runtime overhead. – Ulrich Eckhardt Jan 04 '16 at 11:42
-1
void addElement(Component1 component) {
   componentList1.insert(component);
}

void addElement(Component2 component) {
   componentList2.insert(component);
}
I3ck
  • 433
  • 3
  • 10