0

I have shared pointers of Child objects stored in a Vector of Base shared pointers and I need to dynamically cast the elements of the Base vector to its Child type, such that I can invoke function with Child specific signature.

An example follows below. The first code block defines the class hierarchy and the "identify" functions that I would like to use. The second code block gives a specific example of how I want to invoke a TYPE-specific "identify" function, given that I can cast the original object type from the Base class to a Child class (e.g. A,B,C).

Is there any pattern or technique that I can tackle this issue?

#include <iostream>
#include <memory>
#include <vector>

class Base {};
class A : public Base{};
class B : public Base{};
class C : public Base{};

class CollectionOfBase
{
public:
    void add (std::shared_ptr<Base> item){m_items.push_back(item);}
    std::vector<std::shared_ptr<Base>> const& getItems() const {return m_items;}

private:
    std::vector<std::shared_ptr<Base>> m_items;
};

// I want to use these 3 functions instead of identify( std::shared_ptr<Base> const& )
void identify( std::shared_ptr<A> const& )
{
    std::cout << "A" << std::endl;
}
void identify( std::shared_ptr<B> const& )
{
    std::cout << "B" << std::endl;
}
void identify( std::shared_ptr<C> const& )
{
    std::cout << "C" << std::endl;
}

//This function works in the below for loop, but this is not what I need to use
void identify( std::shared_ptr<Base> const& )
{
    std::cout << "Base" << std::endl;
}

Below, you can find the second code block:

int main()
{
    using namespace std;

    CollectionOfBase collection;

    collection.add(make_shared<A>());
    collection.add(make_shared<A>());
    collection.add(make_shared<C>());
    collection.add(make_shared<B>());

    for (auto const& x : collection.getItems())
    {
        // THE QUESTION:
        // How to distinguish different type of items
        // to invoke "identify" with object specific signatures (e.g. A,B,C) ???
        // Can I cast somehow the object types that I push_back on my Collection ???
        // Note that this loop does not know the add order AACB that we pushed the Child pointers.

        identify(x);
    }
    /*
    The desired output of this loop should be:

    A
    A
    C
    B

    */

    return 0;
}

The code is also available on Ideone.

Barry
  • 286,269
  • 29
  • 621
  • 977
user1221647
  • 65
  • 1
  • 11

3 Answers3

2

Visitor pattern solves the problem.

Basically, add a virtual method Base::accept(Visitor& v).

Each child will override such method calling v.visit(*this).

Your visitor class should look like this:

class Visitor
{
public:
  void visit(A&) { /* this is A */ }
  void visit(B&) { /* this is B */ }
  void visit(C&) { /* this is C */ }
}

Instantiate your visitor: Visitor v;

Iterate on your vector calling x->accept(v);.

http://ideone.com/2oT5S2

csguth
  • 569
  • 3
  • 18
2

There's three approaches you could take here: OO and dynamic dispatch, visitor, and variant. Whichever one is better is going to depend on how many types you have and how many operations you have - as well as which one you're more likely to add to.

  1. Actually use OO. If you need each derived object to perform some operation in a different way, the way to do that in OO is to add a virtual member function:

    struct Base { virtual const char* name() = 0; };
    struct A : Base { const char* name() override { return "A"; }
    // ...
    
    for (auto const& x : collection.getItems()) {
        std::cout << x->name() << std::endl;
    }
    
  2. Use the visitor pattern. This is halfway between OO and functional - we create a base object that knows how to interact with all the types:

    struct Visitor;
    struct Base { virtual void visit(Visitor& ) = 0; };
    struct A;
    struct B;
    struct C;
    
    struct Visitor {
        virtual void visit(A& ) = 0;
        virtual void visit(B& ) = 0;
        virtual void visit(C& ) = 0;
    };
    
    struct A : Base { void visit(Visitor& v) override { v.visit(*this); } };
    // ...
    
    struct IdentityVisitor : Visitor {
        void visit(A& ) { std::cout << "A" << std::endl; }
        void visit(B& ) { std::cout << "B" << std::endl; }
        void visit(C& ) { std::cout << "C" << std::endl; }
    };
    
    IdentityVisitor iv;
    for (auto const& x : collection.getItems()) {
        x->visit(iv);
    }
    
  3. Just use a variant. Instead of storing a collection of shared_ptr<Base>, store a collection of variant<A,B,C> where those types aren't even in a hierarchy. They're just three arbitrary types. And then:

    for (auto const& x : collection.getItems()) {
        visit(overload(
            [](A const& ){ std::cout << "A" << std::endl; },
            [](B const& ){ std::cout << "B" << std::endl; },
            [](C const& ){ std::cout << "C" << std::endl; }
            ), x);
    }
    
Barry
  • 286,269
  • 29
  • 621
  • 977
  • I guess your third approach is a "smarter" implementation for the visitor pattern. – csguth Feb 27 '17 at 15:04
  • @csguth It's just the difference between static visitation and dynamic visitation. There's advantages and disadvantages to both, I don't know that one is strictly better. Certainly if you have a million types but only a small number of operations, both aren't great. – Barry Feb 27 '17 at 15:15
1

@Barry and @csguth thank you both for your answers.

The 3rd option of Barry is very interesting and I would like to give it a try. A working example with boost::static_visitor and boost::variant can be found on this page.

In my case, I did not consider OO and virtual methods as I want to avoid putting logic into these A,B,C objects. With respect to the visitor pattern, it was the only good option that I had in mind. However, I was hoping to discover some more flexible solution like the "LambdaVisitor" and thank you for opening up my eyes!

user1221647
  • 65
  • 1
  • 11