c++ heterogeneous container, get entry as type
Asked Answered
M

2

3

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.

Margerymarget answered 12/12, 2017 at 8:42 Comment(2)
Why "reinvent the wheel" when you can just use std::vector with std::any (or Boost any)? Or perhaps do a redesign so you don't need to use heterogeneous containers or type-erasure?Pandemonium
any can not do that. An any_cast can only cast to the actual type. Not to a base type.Margerymarget
B
2

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";
}
Befall answered 12/12, 2017 at 16:14 Comment(1)
The main problem with this solution is that the elements can't be constructed, and THEN put into the container. I rewrote the rest of my problem to handle this, and it now work perfectly. Answer accepted.Margerymarget
E
2

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

Easternmost answered 12/12, 2017 at 8:49 Comment(6)
this only works if you insert the element with the same type as you retrieve it with. your dynamic_cast will return nullptr, because Holder<B> is not a base class of Holder<S>.Margerymarget
@Xplatformer, ah I missed the "or a base class to it." part ... editing ...Easternmost
@XPlatformer, do you want get<T> to accept a specific set of bases or any base of T ?Easternmost
any base of T, please.Margerymarget
@MassimilianoJanes you might want to define template<class Base, class Derived> bool is_base_of(Derived const&) { return std::is_base_of_v<Base, Derived>; }. test cases on coliru.Practiced
@YSC, why would that help ? if I got OP requirements correctly, get cannot know Derived ( and pushback cannot know Base ) ...Easternmost
B
2

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";
}
Befall answered 12/12, 2017 at 16:14 Comment(1)
The main problem with this solution is that the elements can't be constructed, and THEN put into the container. I rewrote the rest of my problem to handle this, and it now work perfectly. Answer accepted.Margerymarget

© 2022 - 2024 — McMap. All rights reserved.