【问题标题】:Avoid using cast in tree structure避免在树结构中使用强制转换
【发布时间】:2016-06-24 01:45:02
【问题描述】:

我们正在考虑一种需要铸造但感觉不合适的设计。显然用谷歌搜索了这个问题,但没有找到答案。希望获得有关如何避免铸造需要的建议。这是一个精简的例子(请执行任何错别字,这还没有编译)。

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;
};

上述类可用于创建具有多个级别的树结构。

“困难”是开发一种无需强制转换即可遍历此结构的算法。基类中的类型变量应识别正确的类型,并将强制转换的次数减少到单个,但不会避免强制转换。

这个问题的替代设计是什么?

欣赏任何 cmets。

【问题讨论】:

  • std::vector&lt;NodeBase&gt; - 如果您打算在该向量中存储NodeBase(例如NodeValueNodeAggregate)的派生,请三思。那将是object slicing 的经典案例,我严重怀疑这是可取的。
  • 你应该考虑用虚拟方法来实现你所需要的
  • 您很可能正在搜索访问者模式。
  • @Roddy 没有 vtable,因为类型不是多态的。
  • @n.m.:愿意为这个相当大胆的声明提供任何理由吗?

标签: c++ c++11 casting


【解决方案1】:

您似乎要重新实现的模式称为tagged union, or variant,通常与访问者模式配对。我建议您使用现有的实现,而不是自己滚动。

但也要考虑替代实现:

  • 使用同构节点。让每个节点都能够存储子列表和数据。这样你只需要一种类型的节点,不需要强制转换。如果只有叶子可以有数据,那么您可以在算法中实现该限制,而不是在数据结构中。

    struct Node
    {
         std::vector<Node*> m_list;
         std::string m_key;
         std::string m_value;
    };
    
  • 或者使用虚函数:

    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() {}
    };
    

    叶子节点和分支节点可以实现不同的功能。 Leaf 总是可以返回一个空范围,而 branch 可以返回 null 键和值。或者,他们可以要求用户使用is_leaf,并在调用错误类型的函数时抛出异常。

    我没有直接返回对向量的访问,而是使用了range 类型,它应该是一对迭代器,它允许您封装底层容器的选择。

在所有这些设计中,您可以对键和值类型进行模板化以获得更好的通用性。

【讨论】:

    【解决方案2】:

    如果您有多种节点类型,请考虑使用访问者模式遍历所有树节点而不进行强制转换:

    #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);
    }
    

    【讨论】:

      【解决方案3】:
      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;
      

      该变体支持访问,这是一种类型安全的伪静态转换。

      这也消除了所有不必要的间接。

      【讨论】:

      • 这看起来很有趣。
      【解决方案4】:

      我想你想要Visitor 模式。例如:

      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;
          }
      };
      

      这里的诀窍是让访问者类对系统中的每个节点类型都有一个visit 方法,并让每个节点类型都有一个accept 方法来获取访问者并将其自身传递给访问者。这是一种在 C++ 等单一调度语言中实现称为 double dispatch 的技术的方法。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-06-27
        • 2013-06-28
        • 1970-01-01
        • 2014-01-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多