2

We are considering a design that requires casting which does not feel right. Obviously googled the issue but have not found an answer. Would appreciate advice on how to avoid the need for casting. Here is a stripped down example (please execuse any typos, this has not been compiled).

struct NodeBase
{
   enum class type
   {
     value,
     aggregate
   };
   type m_type;
};

struct NodeAggregate : public NodeBase
{
   std::vector<NodeBase*> m_list;
};

struct NodeValue : public NodeBase
{
   std::string m_key;
   std::string m_value;
};

The above classes can be used to create a tree structure with multiple levels.

The 'difficulty' is to develop an algorithm that traverses this structure without casting. The type variable in the base class shall identify the correct type and will reduce the number of cast to a single but it does not avoid casting.

What would be an alternative design for this issue?

Appreciate any comments.

LarsA
  • 595
  • 2
  • 6
  • 14
  • 5
    `std::vector` - if you're intending on storing derivations of `NodeBase` (such as `NodeValue` or `NodeAggregate`) in that vector, think again. That would be a classic case of [object slicing](https://stackoverflow.com/questions/274626/what-is-object-slicing), and I seriously doubt that is desirable. – WhozCraig Jun 23 '16 at 08:32
  • Point taken, code updated. – LarsA Jun 23 '16 at 08:40
  • 4
    you should think about virtual method to achieve what you need – Garf365 Jun 23 '16 at 08:42
  • 2
    You're most probably searching for the visitor pattern. – lorro Jun 23 '16 at 08:44
  • Garf365: Care to explain in more detail what you mean? – LarsA Jun 23 '16 at 08:46
  • Don't use the `type` thing. Prefer `dynamic_cast<>` to that, because type is already effectively stored in the object's vtable. [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself). But, as @Garf365 says, virtual methods are even better. – Roddy Jun 23 '16 at 08:51
  • 1
    @Roddy there is no vtable because the types are not polymorphic. – eerorika Jun 23 '16 at 08:56
  • [Composite design pattern](http://ikeptwalking.com/composite-design-pattern-explained/) seems appropriate here. – Maxim Egorushkin Jun 23 '16 at 09:37
  • If you inherit without having a virtual function in the base, you are doing it wrong. – n. m. could be an AI Jun 23 '16 at 10:02
  • 1
    @n.m.: Care to provide any reasoning for that pretty bold statement? – MFH Jun 23 '16 at 10:11
  • @MFH What's so bold about it? Inheritance is there to support OOP, virtual functions are necessary for OOP. If you are using inheritance for something other than OOP, you are using a tool for a purpose it is not designed for. – n. m. could be an AI Jun 23 '16 at 11:21
  • 1
    @n.m.: It **IS** pretty bold to claim that OOP requires virtual functions! – MFH Jun 23 '16 at 14:43
  • @MFH No, it is an absoluttely trivial statement. – n. m. could be an AI Jun 23 '16 at 14:50
  • @n.m.: And simply wrong! The whole standards library is a perfect example of why your definition of OOP is simply WRONG! – MFH Jun 23 '16 at 15:52
  • @MFH The standard library is not object-oriented. Alex Stepanov says so, shall we trust him? – n. m. could be an AI Jun 23 '16 at 15:58
  • @n.m. 1.: The standard library is NOT the work of Stepanov, he is "only" the inventor of the STL. 2.: The standard library is very much object oriented... It's not Java-style, which is probably what you consider the only form of OOP... – MFH Jun 23 '16 at 16:03
  • @MFH STL is not object-oriented (that's actually a quote from Stepanov). Other parts of the standard library like iostreams are in part object oriented... and use virtual functions. These parts are marginal. I also trust Alan Kay when he says what's object oriented: "OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late binding of all things." Late binding = virtual functions. (Kay doesn't think C++ is an OO language). What do *you* call object-oriented? – n. m. could be an AI Jun 23 '16 at 20:52
  • @n.m.: Alan Kay is NOT the inventor of OOP. he only came up with the term "OOP". Simula was the first OOP language and by Kay's standard, it's not object oriented... BTW: Stroustrup (you know, the inventor of C++) considers C++ a language that supports OOP and he didn't want to include virtual functions initially. – MFH Jun 24 '16 at 09:28
  • @MFH You are still nit saying what's your definition of OOP. Perhaps you can defune it by example. Can you point at a programming language ir other software that does not use late bindings but is nevertheless widely regarded as object-oriented? – n. m. could be an AI Jun 24 '16 at 12:27

4 Answers4

2

The pattern that you seem to be re-implementing is called tagged union, or variant, which is typically paired with the visitor pattern. Instead of rolling your own, I recommend using an existing implementation.

But also consider alternative implementations:

  • Use homogeneous nodes. Let every node be able to store both child list, and data. That way you only need one type of node and no casting is necessary. If only leaves can have data, then you can implement that limitation in the algorithms, instead of the data structure.

    struct Node
    {
         std::vector<Node*> m_list;
         std::string m_key;
         std::string m_value;
    };
    
  • Or use virtual functions:

    struct NodeBase
    {
        virtual       bool  is_leaf()            = 0;
        virtual const range children()     const = 0;
        virtual       range children()           = 0;
        virtual const std::string* key()   const = 0;
        virtual       std::string* key()         = 0; // if the tree is not sorted by key
        virtual const std::string* value() const = 0;
        virtual       std::string* value()       = 0;
        virtual ~NodeBase() {}
    };
    

    The leaf and branch nodes can implement the functions differently. Leaf can always return an empty range, while branch can return null key and value. Alternatively, they can require the user to use is_leaf, and throw an exception if wrong type of function is called.

    Instead of returning access to the vector directly, I used a range type, that should be a pair of iterators, which allows you to encapsulate the choice of the underlying container.

In all of these designs, you can templetize the key and value types for better generality.

eerorika
  • 232,697
  • 12
  • 197
  • 326
1

If you have several node types consider to use Visitor Pattern to traverse through all tree nodes without casting:

#include <iostream>
#include <memory>
#include <string>

class Aggregate;
class ConcreteNode1;
class ConcreteNode2;

class Visitor {
public:
  virtual void Visit(ConcreteNode1& node) = 0;
  virtual void Visit(ConcreteNode2& node) = 0;
  virtual void Start(Aggregate& aggregate) = 0;
  virtual void Finish(Aggregate& aggregate) = 0;
};

class Node {
  friend class Aggregate;
public:
  Node() : parent_(nullptr) {}
  virtual ~Node() = default;
  virtual void Accept(Visitor& visitor) = 0;
private:
  void SetParent(Aggregate* parent) {
    parent_ = parent;
  }
  Aggregate* parent_;
};

class Aggregate : public Node {
public:
  void Add(std::shared_ptr<Node> node) {
    node->SetParent(this);
    nodes_.push_back(std::move(node));
  }
  virtual void Accept(Visitor& visitor) override {
    visitor.Start(*this);
    for (auto node : nodes_) {
      node->Accept(visitor);
    }
    visitor.Finish(*this);
  }
private:
  std::vector<std::shared_ptr<Node>> nodes_;
};

class ConcreteNode1 : public Node {
public:
  ConcreteNode1(int data) : data_(data) {}
  virtual void Accept(Visitor& visitor) override {
    visitor.Visit(*this);
  }
  int GetData() const { return data_; }
private:
  int data_;
};

class ConcreteNode2 : public Node {
public:
  ConcreteNode2(std::string name) : name_(std::move(name)) {}
  virtual void Accept(Visitor& visitor) override {
    visitor.Visit(*this);
  }
  const std::string& GetName() const { return name_; }
private:
  std::string name_;
};

int main()
{
  class SimpleVisitor : public Visitor {
    virtual void Visit(ConcreteNode1& node) override {
      std::cout << "ConcreteNode1: " << node.GetData() << std::endl;
    }
    virtual void Visit(ConcreteNode2& node) override {
      std::cout << "ConcreteNode2: " << node.GetName() << std::endl;
    }
    virtual void Start(Aggregate& aggregate) override {
      std::cout << "Start Aggregate\n";
    }
    virtual void Finish(Aggregate& aggregate) override {
      std::cout << "Finish Aggregate\n";
    }
  } visitor;
  auto root = std::make_shared<Aggregate>();
  root->Add(std::make_shared<ConcreteNode1>(1));
  {
    auto subtree = std::make_shared<Aggregate>();
    subtree->Add(std::make_shared<ConcreteNode1>(2));
    subtree->Add(std::make_shared<ConcreteNode2>("test1"));
    root->Add(subtree);
  }
  root->Add(std::make_shared<ConcreteNode2>("test2"));

  /// traverse through all nodes
  root->Accept(visitor);
}
AnatolyS
  • 4,249
  • 18
  • 28
0

I think you want the Visitor pattern. For example:

struct NodeAggregate;
struct NodeValue;

struct Visitor
{
    virtual void visit(NodeAggregate &aggregate) = 0;
    virtual void visit(NodeValue &value) = 0;
};

struct NodeBase
{
    virtual void accept(Visitor &visitor) = 0;
};

struct NodeAggregate : public NodeBase
{
    std::vector<NodeBase*> m_list;

    void accept(Visitor &visitor) override
    {
        visitor.visit(*this);

        for(auto base : m_list)
        {
            base->accept(visitor);
        }
    }
};

struct NodeValue : public NodeBase
{
    std::string m_key;
    std::string m_value;

    void accept(Visitor &visitor) override
    {
        visitor.visit(*this);
    }
};

struct WriteToConsoleVisitor : Visitor
{
    void visit(NodeAggregate &aggregate) override
    {
        std::cout << "got an aggregate" << std::endl;
    }

    void visit(NodeValue &value) override
    {
        std::cout << "got a value. key = " << value.m_key << ", value = " << value.m_value << std::endl;
    }
};

The trick here is to have a visitor class that has a visit method for each node type in the system, and to have each node type have an accept method that takes the visitor and passes itself into the visitor. This is a way of implementing a technique called double dispatch in single dispatch languages like C++.

Sean
  • 60,939
  • 11
  • 97
  • 136
0
struct NodeBase;

struct NodeAggregate {
  std::vector<std::unique_ptr<NodeBase>> children;
  ~NodeAggregate();
};

struct NodeValue {
  std::string key, value;
};

struct NodeBase {
  boost::variant<NodeAggregate, NodeValue> m;
};

inline NodeAggregate::~NodeAggregate() = default;

the variant supports visiting, which is a type-safe pseudo static-cast.

This also eliminates all needless indirection.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524