c++ heterogeneous container, get entry as type

1.4k views Asked by At

I have the following simple implementation of a Heterogeneous container:

struct Container {
    struct HolderBase {
    };

    template<typename S>
    struct Holder : HolderBase {
        Holder(S* s) : s_(s) {}
        S* s_;
    };

    template<typename S>
    void push_back(S* s) {
        h_.push_back(new Holder<S>(s));
    }

    vector<HolderBase*> h_;

    template<typename B>
    B* get(int i) {
        //magic here
    }
};

Here's how to use it:

struct ElementBase {};
struct Element : ElementBase {};

int main()
{
    Container container;
    container.push_back(new Element);
    ElementBase* elementBase = container.get<ElementBase>(0);
}

I can add entries of any type to it. But I can't figure out how to implement a function to retrieve elements, as some type, which may be the same as the entry or a base class to it.

What I need seems to be both virtual and template at the same time, which is not possible.

2

There are 2 answers

1
n. m. could be an AI On BEST ANSWER

It doesn't seem possible to have exactly what you want without much pain and inconvenience (for example, registering all classes you want to work with in some kind of central repository).

Here's one way to do almost what you want that can perhaps be useful.

class HolderBase
{
  public:
    virtual ~HolderBase() = default;    
    template <class X> X* get() { return dynamic_cast<X*>(this); }
};

template <class T>
class Holder : public HolderBase, public T
{
  public:
    using T::T;
};

Your container is then just a vector<unique_ptr<HolderBase>> or whatever bunch-of-pointers you fancy.

Test drive:

struct A {
    virtual ~A() = default;
    A(int a) : a(a) {};
    int a;
};

struct B : A {
    B(int a, int b) : A(a), b(b) {};
    int b;
};

struct C : A {
    C(int a, int c) : A(a), c(c) {};
    int c;
};


int main () {
    std::vector<std::unique_ptr<HolderBase>> v;
    v.emplace_back(std::make_unique<Holder<B>>(7,40));
    v.emplace_back(std::make_unique<Holder<C>>(0,42));

    A* a = v[0]->template get<A>();
    B* b = v[0]->template get<B>();
    C* c = v[0]->template get<C>();

    std::cout << a << " " << b << " " << c << "\n";

    a = v[1]->template get<A>();
    b = v[1]->template get<B>();
    c = v[1]->template get<C>();

    std::cout << a << " " << b << " " << c << "\n";
}
6
Massimiliano Janes On

how to implement a function to retrieve elements, as some type, which may be the same as the entry or a base class to it.

To get the same entry, the simplest way preserving your current design is to use RTTI.

firstly, make the type-erasing base polymorphic:

struct HolderBase { virtual ~HolderBase() = default; };

then, you can just dynamic_cast:

template<typename B>
B* get(int i) {
    if( auto holder = dynamic_cast<Holder<B>*>(h_[i]) )
    {
       return holder->s_;
    }
    else
       return nullptr;
}

this will return nullptr whenever the dynamic type of the object pointed to by h_[i] is the wrong type. You may also throw or provide a throwing get<B&> overload as well.

Note that in C++17, we already have std::any (originated from boost.any) basically doing the same, but with a standard interface (to become idiomatic soon) and all details already worked out; so, it's strongly advisable to use it instead of rolling out your own.


The problem of getting an entry as a base is harder though; the simplest solution is to pass the allowed target types to pushback, something like:

template<typename... T,typename S>
void push_back_as(S* s) {
   static_assert( ( std::is_base_of_v<T,S> && ...) );

   h_.push_back(new Holder<S,T...>(s)); // where Holder<S,T0,...> inherits from properly defined Holder<S>,Holder<T0>,...
}

or use some other non-intrusive means for registering the target types (eg. a trait class).

otherwise, I don't think this is generally possible as of now (it will be when we'll have compile-time reflection).