0

I have the following situation:

template <typename T>
class Derived
{
public:
    T m_Data;
};

And I need to store them in std::vector so I've introduced an abstract parent class like so:

class Base
{
public:
  virtual unsigned int GetID() const = 0;
};

And made Derived inherit from Base:

template <typename T>
class Derived : public Base
{
public:
    T m_Data;
    unsigned int GetID() const override
    {
        return something;
    }
}

So now I can store them in std::vector like so:

std::vector<Base*> m_Bases;

Now at some point of program execution I need to iterate over m_Bases and select a few based on some condition and use their respective m_Data like so:

void Foo()
{
    for (auto& b : m_Bases)
    {
        if (some_condition)
        {
            // Access b->m_Data
        }
    }
}

Since I can't do that because T m_Data is member of Derived and not Base, I introduced a visitor vector:

std::vector<std::function<void(Base*, std::function<void(std::any)>&)>> m_DerivedVisitors;

Now when I insert a new Derived<T> to m_Bases I also insert a new lambda to m_DerivedVisitors so I could call later like so:

template <typename T>
void CreateBase()
m_Bases.push_back(new Derived<T>());
m_DerivedVisitors.push_back([](Base* base, auto visitor)
{
    if (dynamic_cast<Derived<T>*>(base) != nullptr)
    {
        visitor(static_cast<Derived<T>*>(base));
    }
});

Now Foo could potentially get a Derived instance in a visitor generic lambda (I hope) like so:

void Foo()
{
    for (auto& b : m_Bases)
    {
        if (some_condition)
        {
            // Access b->m_Data
            // Note that I made sure GetID() is returning counting index based on template instantiation 
            // So I could use it to index into m_Bases as-well as into m_DerivedVisitors
            m_DerivedVisitors[b->GetID()](base, [&](auto derivedPtr) {
                // Now I can access derivedPtr->m_Data and use outside variable with capture list
            });
        }
    }
}

Now if you have sharp eyes you can see I have a few problems with this code.
I don't know what type the second argument of m_DerivedVisitors should be.
Right now it's set to std::function<void(std::any)>& because I do not know the type of the visitor because it's argument supposed to be the Derived template type so I tried to use std::any and for the visitor it-self I used an auto parameter making it a generic lambda:

std::vector<std::function<void(Base*, std::function<void(std::any)>&)>> m_DerivedVisitors;
...
            m_DerivedVisitors[b->GetID()](base, [&](auto derivedPtr) {
                // Now I can access derivedPtr->m_Data and use outside variable with capture list
            });
...

I'm not sure how I can make it work.
I'll would be appreciate if somebody could explain how this can be done with a code example.

Thanks in advance.

UPDATE:
I thought of a solution that I don't like at all but I'll present anyway, maybe it could help someone to help me on improving it or finding another solution. We change m_DerivedVisitors to this:

std::vector<std::function<void(Base*)>> m_DerivedVisitors;

and we push to it like this:

m_DerivedVisitors.push_back([](Base* base)
{
    if (dynamic_cast<Derived<T>*>(base) != nullptr)
    {
        Visit(static_cast<Derived<T>*>(base));
    }
});

Note we scraped the auto visitor function parameter and we call now a static Visit method defined as follows:

template<typename U>
static inline void Visit(U* object)
{
    auto x = object->m_Data;
    // Now we can do what ever we like with m_Data
}

The problem I see with this solution is that I can't be specific with what visitor I use. One way to solve this is change m_DerivedVisitors again to the following:

std::vector<std::function<void(Base*, unsigned int)>> m_DerivedVisitors;

and change the way we push to it like this:

m_DerivedVisitors.push_back([](Base* base, unsigned int id)
{
    if (dynamic_cast<Derived<T>*>(base) != nullptr)
    {
        switch(id)
        {
        case 0:
            Visit0(static_cast<Derived<T>*>(base));
            break;
        case 1:
            Visit1(static_cast<Derived<T>*>(base));
            break;
        ...
        };
    }
});

As you can see we have to define all visitors as static functions and we have to choose them using an id. We can't even do this with std::unordered_map because we can't have std::function pointing to a template function.

Now whenever something wants to use the derived type for a specific task we'll have to define a static function and add it to the switch case and call it with the appropriate id.

This is not the only problem.
Originally in my code Foo was given a template lambda argument as-well that I wanted to pass m_Data to it, here's how:

template <typename Fn>
void Foo(Fn&& fn)
{
    for (auto& b : m_Bases)
    {
        if (some_condition)
        {
            // Access b->m_Data and pass it to fn
            fn(b->m_Data); // Note this doesn't compile; just to demonstrate my intentions
        }
    }
}

Now in-order to do that with the solution I came up with I would have to store fn in some static/global variable and access it inside the visitor function like so:

static int y = 8;

template<typename U>
static inline void Visit0(U* object)
{
    // access y
    auto x = object->m_Data;
}

template <typename Fn>
void Foo(Fn&& fn)
{
    for (auto& b : m_Bases)
    {
        if (some_condition)
        {
            // Access b->m_Data and pass it to fn
            y = 5;
            m_DerivedVisitors[b->GetID()](b, 0);
        }
    }
}

Note that this would not work for captures of template type which is not going to work for my purpose with Fn&& fn since I can't have static/global variable of that type obviously since it's a different scope completetly. And not to mention I would have to do that for every kind of visitor that would want to introduce other variables into the visit function, that is because I can't use lambda with capture lists as my original plan was.

So as you can see this solution is not ideal at all, it's just an idea.
Hopefully someone could think of a better approach or guide me to something else.

Jorayen
  • 1,737
  • 2
  • 21
  • 52
  • It seems that the decision to store ptrs to base drives you into lots of problems. Perhaps it's worth working with `variant`- if you don't have too many possible template arguments for your template? – Igor R. May 08 '20 at 20:42
  • I can't know what types at compile time would be since a user of this interface could use any type `T` he wants – Jorayen May 08 '20 at 20:58
  • If you know what to do following the template type T, why not keeping the behavior in a private member function of Derived, and then calling a virtual function over m_Bases to check/apply the callback dynamically ? In general, dynamic casting is when you don't want to accept a behavior to be part of a type, implying to handle it later as you want to, which is understandable but not practical. You might want to choose between pure dynamic polymorphism or pure static polymorphism to avoid storing heterogenous data. – Cevik May 09 '20 at 23:02
  • @Cevik `Foo` is receiving a generic lambda as a template argument. I need to pass `m_Data` to it eventually. I can't make a virtual function that accept template argument, that's why. – Jorayen May 09 '20 at 23:07
  • Since m_Data belongs to Derived, i think that any generic lambda need is a missing functionality of Derived because it's breaking encapsulation. Even if each template instantiation is a different type, templates reflect the desire to handle everything at once. With the new features like requirements and constexpr if, it is even easier to do what you want in Derived. The user of Foo can be anyone, which is not very safe for Derived in any case. – Cevik May 09 '20 at 23:52
  • Well I agree if we were talking about OOP. But my actual domain of problem is DOD where I try to create an ECS. you can think of `Base` as `Archetype`, `Derived` as `Archetype` and `Foo` as a `Query` where a lambda is used to ask for `Archetype`s with set of components. Now in traditional OOP you would be right but in this domain of problem it would be just achieving it purpose and no more. The reason I didn't mention it is because I don't see this question as "is this breaking encapsulation" question, I only want to achieve something with the tools I have that's it. – Jorayen May 10 '20 at 00:32

0 Answers0