【问题标题】:Transforming trees in C++在 C++ 中转换树
【发布时间】:2014-12-30 15:29:03
【问题描述】:

我有一个由不同类型的节点组成的异构 n 叉树,例如:

class Node { }

class Subnode1 : public Node
{

}

class Subnode2 : public Node
{
private:
  unique_ptr<Subnode1> child;

public:
  Subnode1* getChild() { return child.get(); }
}

class Subnode4 : public Subnode2 { }

class Subnode3 : public Node
{
private:
  list<unique_ptr<Subnode2>> children;
public:
  // getter
}

// etc

该结构可以包含任意数量的节点类型,并将在未来进行扩展。

我实现了一种访问树的方法,它允许我自定义访问每个节点时要做什么,基本上是这样实现的:

void genericVisit(Node* node) {
  if (dynamic_cast<Subnode2>(node))
    visit(static_cast<Subnode2*>(node));
  // etc
}

virtual void visit(Subnode2* node) {
  if (node->getChild())
    genericVisit(node->getChild());
}

// etc

它可以很好地遍历树,但现在我需要能够用其他子树替换子树,所以我正在考虑在不过多改变结构的情况下遵循的最佳方法。

最好的解决方案是让 getter 直接返回unique_ptr&lt;Subnode2&gt;&amp;,这样我就可以对唯一指针进行访问,并且可以在访问它时更改智能指针的内容。这可以工作,但unique_ptr 不是多态的,并且无法将调用分派给最专门的方法,例如:

void genericVisit(unique_ptr<Node>& node) { // NON WORKING CODE
  if (dynamic_cast<unique_ptr<Subnode2>&>(node))
    visit(static_cast<unique_ptr<Subnode2>&>(node));
  // etc
}

所以我想知道哪个可能是问题的最佳解决方案,注意在遍历树时,只要我更改当前子树的内容而不更改对父节点中该节点的引用,(这将是使用 unique_ptr 允许)我没有发现任何问题。

【问题讨论】:

  • 如果您使用std::shared_ptr 而不是std::unique_ptr 不会破坏任何东西,您可以使用dynamic_pointer_caststatic_pointer_cast 来执行转换操作。
  • @YoungJohn:我虽然知道shared_ptr,但它不起作用。 dynamic_pointer_cast 会复制共享指针,因此修改副本不会影响存储在树中的原始 shared_ptr
  • @Jack 你可以在(Sean Parent: should) 那里使用shared_pointer&lt;const Node&gt;,并根据需要进行复制以进行更改。不变的胜利!也就是说,也许您可​​以完全不使用继承?让事情变得更简单,并可能更高效

标签: c++ algorithm c++11 tree visitor-pattern


【解决方案1】:

因为我认为评论不足以传达所有信息:

@Jack 你可以在那里(Sean Parent: should) 使用shared_pointer&lt;const Node&gt;,并根据需要进行复制以进行更改。不变的胜利!也就是说,也许您可​​以完全不使用继承?让事情变得更简单,可能更高效——sehe 6 hours ago

我想我会以相反的顺序稍微演示一下这两个方面:

  1. 关于对树使用静态多态性:让我们定义一棵树,其中叶节点可以是字符串或用户定义的结构:

    struct Data {
        double x,y,z;
    };
    
    using Node = make_recursive_variant<std::string, Data, std::vector<recursive_variant_> >::type;
    using Nodes = std::vector<Node>;
    

    以下所有测试中使用的示例树现在定义为:

    Node tree = Nodes {
        "hello",
        Data { 1,2,3 },
        Nodes {
            "more nested",
            Nodes {
                Data { 2,3,4 },
                Data { 3,4,5 },
                Data { 4,5,6 },
            },
            "nodes"
        }
    };
    

    Boost Variant 非常适合通过访问进行转换。例如,让我们编写一个访问者,它反转树中的所有节点,因此包括字符串和Data{x,y,z} -> Data{z,y,x}

    struct reverser : static_visitor<Node> {
        Node operator()(Node const& tree)      const { return apply_visitor(*this, tree); }
        Node operator()(std::string const& s)  const { return std::string(s.rbegin(), s.rend()); }
        Node operator()(Data const& d)         const { return Data {d.z, d.y, d.x}; }
        Node operator()(Nodes const& children) const {
            Nodes revchildren;
            std::transform(children.rbegin(), children.rend(), back_inserter(revchildren), *this);
            return revchildren;
        }
    };
    
    Node reverse(Node const& tree) {
        return reverser()(tree);
    }
    

    查看现阶段的简化演示Live On Coliru

    • 我添加了一个print 访问者,以便您查看结果
    • 它甚至对树进行往返测试 (tree == reverse(reverse(tree)))

  2. 现在您会注意到 1. 下的树具有值语义,这意味着反向操作会生成完全独立的树副本。

    我继续添加了一个类似的SharedTree

    using SNode = boost::shared_ptr<
            boost::make_recursive_variant<
                std::string, Data, std::vector<boost::shared_ptr<boost::recursive_variant_>
            > >::type>;
    
    using Node  = SNode::element_type;
    using Nodes = std::vector<SNode>;
    

    当您操作这些树的副本时,两个副本都会发生变化,因为它们共享节点(下面的演示程序将一个节点添加到一个 SharedTree 并观察到另一个也发生了变化)

  3. 现在是Sean Parent 方法:使用shared_ptr&lt;T const&gt;

    namespace CowTree {
        using SNode = boost::shared_ptr<
                boost::make_recursive_variant<
                    std::string, Data, std::vector<boost::shared_ptr<boost::recursive_variant_ const>
                > >::type const>;
    
        using Node  = SNode::element_type;
        using Nodes = std::vector<SNode>;
    }
    

    这样做的好处是复制成本很低,但你不可能修改一个节点:你总是需要深度复制一个子树来修改它的任何方面的值。为了证明这一点,我做了一个转换,只将 string 叶节点大写,其他所有内容保持不变:

    template <typename SNode, typename Nodes = std::vector<SNode> >
    struct upper_caser : boost::static_visitor<SNode> {
        SNode operator()(SNode const& tree)            const { 
            return apply_visitor(boost::bind(*this, boost::ref(tree), _1), *tree); 
        }
        // dispatch
        SNode operator()(SNode const&, std::string const& value) const {
            std::string xformed;
            std::transform(value.begin(), value.end(), back_inserter(xformed), [](uint8_t c) { return std::toupper(c); });
            return boost::make_shared<typename SNode::element_type>(xformed);
        }
        SNode operator()(SNode const& node, Nodes const& children)  const {
            Nodes xformed; // TODO optimize
            std::transform(children.begin(), children.end(), back_inserter(xformed), *this);
    
            return (equal(children, xformed))
                ? node
                : boost::make_shared<typename SNode::element_type>(xformed);
        }
        template <typename V> SNode operator()(SNode const& node, V const&) const {
            return node;
        }
    };
    
    template <typename SNode>
    SNode ucase(SNode const& tree) {
        return upper_caser<SNode>()(tree);
    }
    

    正如您所见,转换是完全通用的,并且可以与SharedTreeCowTree 一起使用——因为没有任何值会发生突变。显然,使用CowTree 更安全,因为它保证了不变性。

    测试程序主动检查

    • 当我们得到 ucased 变换时,原始的 cow 树并没有改变。
    • 它还会检查没有改变的子树确实是共享的(没有被克隆)

完整程序

Live On Coliru

#include <boost/variant.hpp>

namespace Tree {
    struct Data {
        double x,y,z;
    };

    using Node  = boost::make_recursive_variant<std::string, Data, std::vector<boost::recursive_variant_> >::type;
    using Nodes = std::vector<Node>;

    namespace Operations {
        struct reverser : boost::static_visitor<Node> {
            Node operator()(Node const& tree)       const { return apply_visitor(*this, tree); }
            Node operator()(Data const& d)          const { return Data {d.z, d.y, d.x}; }
            Node operator()(std::string const& s)   const { return std::string(s.rbegin(), s.rend()); }
            Node operator()(Nodes const& children)  const {
                Nodes revchildren;
                std::transform(children.rbegin(), children.rend(), back_inserter(revchildren), *this);
                return revchildren;
            }
        };

        Node reverse(Node const& tree) {
            return reverser()(tree);
        }
    }

    using Operations::reverse;
}

// For our demo, let's implement `operator <<` with a manipulator
#include <iostream>
namespace IO {
    namespace detail {
        using namespace boost;

        // this pretty prints the tree as C++ initializer code
        struct print_visitor : static_visitor<void> {
            print_visitor(std::ostream& os, std::string const& indent = "\n") : _os(os), _indent(indent) {}

            // concrete types
            void operator()(std::string const& s)  const { _os << '"' << s << '"'; }
            void operator()(Tree::Data const& d)   const { _os << "Data {" << d.x << "," << d.y << "," << d.z << '}'; }

            // generics to cater for both direct and shared tree nodes
            template <typename T>     void operator()(shared_ptr<T> const& sp)       const { (*this)(*sp); }
            template <typename T>     void operator()(shared_ptr<const T> const& sp) const { (*this)(*sp); }
            template <typename... Ts> void operator()(variant<Ts...> const& tree)    const { apply_visitor(*this, tree); }

            template <typename Node>
            void operator()(std::vector<Node> const& children) const {
                _os << "Nodes {";

                print_visitor subnode(_os, _indent + "  ");
                for(auto& n : children) {
                    _os << subnode._indent;
                    subnode(n);
                    _os << ",";
                }

                _os << _indent << '}';
            }
        private:
            std::ostream& _os;
            mutable std::string _indent;
        };

        template <typename NodeType>
        struct print_manip {
            print_manip(NodeType const& n) : _node(n) {}
            friend std::ostream& operator<<(std::ostream& os, print_manip const& m) {
                return print_visitor(os)(m._node), os << ";";
            }

            private:
            NodeType const& _node;
        };
    }

    template <typename NodeType>
        detail::print_manip<NodeType> print(NodeType const& node) { 
            return node; 
        }
}

#include <boost/make_shared.hpp>

namespace Support {
    namespace detail {
        template <typename SNode, typename Nodes = std::vector<SNode> >
        struct share_visitor : boost::static_visitor<SNode> {
            SNode operator()(Tree::Node const& tree)      const { return apply_visitor(*this, tree); }

            SNode operator()(Tree::Nodes const& children) const {
                Nodes shared;
                std::transform(children.begin(), children.end(), back_inserter(shared), *this);
                return boost::make_shared<typename SNode::element_type>(shared);
            }

            template <typename LeafNode>
            SNode operator()(LeafNode const& v) const { return boost::make_shared<typename SNode::element_type>(v); }
        };
    }
}

#include <boost/bind.hpp>

namespace SharedTree {
    using Tree::Data;

    using SNode = boost::shared_ptr<
            boost::make_recursive_variant<
                std::string, Data, std::vector<boost::shared_ptr<boost::recursive_variant_>
            > >::type>;

    using Node  = SNode::element_type;
    using Nodes = std::vector<SNode>;

    namespace Operations {
        template <typename SNode, typename Nodes = std::vector<SNode> >
        struct upper_caser : boost::static_visitor<SNode> {
            SNode operator()(SNode const& tree)            const { 
                return apply_visitor(boost::bind(*this, boost::ref(tree), _1), *tree); 
            }
            // dispatch
            SNode operator()(SNode const&, std::string const& value) const {
                std::string xformed;
                std::transform(value.begin(), value.end(), back_inserter(xformed), [](uint8_t c) { return std::toupper(c); });
                return boost::make_shared<typename SNode::element_type>(xformed);
            }
            SNode operator()(SNode const& node, Nodes const& children)  const {
                Nodes xformed; // TODO optimize
                std::transform(children.begin(), children.end(), back_inserter(xformed), *this);

                return (equal(children, xformed))
                    ? node
                    : boost::make_shared<typename SNode::element_type>(xformed);
            }
            template <typename V> SNode operator()(SNode const& node, V const&) const {
                return node;
            }
        };

        template <typename SNode>
        SNode ucase(SNode const& tree) {
            return upper_caser<SNode>()(tree);
        }
    }

    using Operations::ucase;

    SNode share(Tree::Node const& tree) {
        return Support::detail::share_visitor<SNode>()(tree);
    }
}

namespace CowTree {
    using Tree::Data;

    using SNode = boost::shared_ptr<
            boost::make_recursive_variant<
                std::string, Data, std::vector<boost::shared_ptr<boost::recursive_variant_ const>
            > >::type const>;

    using Node  = SNode::element_type;
    using Nodes = std::vector<SNode>;

    SNode share(Tree::Node const& tree) {
        return Support::detail::share_visitor<SNode>()(tree);
    }
}

#include <boost/lexical_cast.hpp> // for the roundtrip test

namespace Support {
    template <typename NodeType1, typename NodeType2>
    bool tree_equal(NodeType1 const& a, NodeType2 const& b) {
        using IO::print;
        return boost::lexical_cast<std::string>(print(a)) == 
            boost::lexical_cast<std::string>(print(b));
    }
}

int main() {
    using namespace Tree;
    using IO::print;
    using Support::tree_equal;

    Node tree = Nodes {
        "hello",
        Data { 1,2,3 },
        Nodes {
            "more nested",
            Nodes {
                Data { 2,3,4 },
                Data { 3,4,5 },
                Data { 4,5,6 },
            },
            "nodes"
        }
    };

    std::cout << "Before transformation: \n" << print(tree)          << "\n";
    std::cout << "After transformation:  \n" << print(reverse(tree)) << "\n";

    Node roundtrip = reverse(reverse(tree));
    std::cout << "Roundtrip tree_equal: "    << std::boolalpha       << tree_equal(tree, roundtrip) << "\n";

    std::cout << "//////////////////////////////////////////////////\n";
    std::cout << "// manipulate SharedTree \"copies\"\n";
    auto shared = SharedTree::share(tree);
    std::cout << "Shared: "          << print(shared)            << "\n";
    std::cout << "Equal to source: " << tree_equal(tree, shared) << "\n";

    auto shared2 = shared;

    using boost::get;
    using boost::make_shared;
    get<SharedTree::Nodes>(*shared).push_back(make_shared<SharedTree::Node>("added to a shared tree"));

    std::cout << "Shared2 after changing shared: " << print(shared2)              << "\n";
    std::cout << "Shared trees equal: "            << tree_equal(shared, shared2) << "\n";
    std::cout << "Not equal to source: "           << tree_equal(tree, shared)    << "\n";

    std::cout << "//////////////////////////////////////////////////\n";
    std::cout << "// now let's see about CowTree\n";
    auto cow = CowTree::share(tree);
    std::cout << "Cow: "                 << print(cow)            << "\n";
    std::cout << "Equal to source: "     << tree_equal(tree, cow) << "\n";

    auto ucased = SharedTree::ucase(cow);
    std::cout << "Ucased: "              << print(ucased)           << "\n";
    std::cout << "Equal to cow source: " << tree_equal(ucased, cow) << "\n";

   /*
    *    The 
    *
    *        Nodes {
    *            Data { 2,3,4 },
    *            Data { 3,4,5 },
    *            Data { 4,5,6 },
    *        },
    *
    *    subtree should still be shared, because it wasn't touched:
    */
    std::cout << "Subtree from ucased: " << print(get<CowTree::Nodes>(*get<CowTree::Nodes>(*ucased)[2])[1]) << "\n";
    std::cout << "Subtree from cow: "    << print(get<CowTree::Nodes>(*get<CowTree::Nodes>(*cow   )[2])[1]) << "\n";
    std::cout << "Subtrees match: "      << tree_equal(
            get<CowTree::Nodes>(*get<CowTree::Nodes>(*ucased)[2])[1],
            get<CowTree::Nodes>(*get<CowTree::Nodes>(*cow)   [2])[1])
        << "\n";
    // unchanged nodes should be shared:
    std::cout << "Subtrees shared: " << (
            get<CowTree::Nodes>(*get<CowTree::Nodes>(*ucased)[2])[1].get() ==
            get<CowTree::Nodes>(*get<CowTree::Nodes>(*cow)   [2])[1].get())
        << "\n";
    // changed nodes aren't shared:
    std::cout << "Siblings unshared: " << (
            get<CowTree::Nodes>(*get<CowTree::Nodes>(*ucased)[2])[2].get() !=
            get<CowTree::Nodes>(*get<CowTree::Nodes>(*cow)   [2])[2].get())
        << "\n";
    std::cout << "Parents unshared: " << (
            get<CowTree::Nodes>(*ucased)[2].get() !=
            get<CowTree::Nodes>(*cow)   [2].get())
        << "\n";
    std::cout << "Roots unshared: " << ( ucased.get() != cow.get())
        << "\n";
}

输出:

Before transformation: 
Nodes {
  "hello",
  Data {1,2,3},
  Nodes {
    "more nested",
    Nodes {
      Data {2,3,4},
      Data {3,4,5},
      Data {4,5,6},
    },
    "nodes",
  },
};
After transformation:  
Nodes {
  Nodes {
    "sedon",
    Nodes {
      Data {6,5,4},
      Data {5,4,3},
      Data {4,3,2},
    },
    "detsen erom",
  },
  Data {3,2,1},
  "olleh",
};
Roundtrip tree_equal: true
//////////////////////////////////////////////////
// manipulate SharedTree "copies"
Shared: Nodes {
  "hello",
  Data {1,2,3},
  Nodes {
    "more nested",
    Nodes {
      Data {2,3,4},
      Data {3,4,5},
      Data {4,5,6},
    },
    "nodes",
  },
};
Equal to source: true
Shared2 after changing shared: Nodes {
  "hello",
  Data {1,2,3},
  Nodes {
    "more nested",
    Nodes {
      Data {2,3,4},
      Data {3,4,5},
      Data {4,5,6},
    },
    "nodes",
  },
  "added to a shared tree",
};
Shared trees equal: true
Not equal to source: false
//////////////////////////////////////////////////
// now let's see about CowTree
Cow: Nodes {
  "hello",
  Data {1,2,3},
  Nodes {
    "more nested",
    Nodes {
      Data {2,3,4},
      Data {3,4,5},
      Data {4,5,6},
    },
    "nodes",
  },
};
Equal to source: true
Ucased: Nodes {
  "HELLO",
  Data {1,2,3},
  Nodes {
    "MORE NESTED",
    Nodes {
      Data {2,3,4},
      Data {3,4,5},
      Data {4,5,6},
    },
    "NODES",
  },
};
Equal to cow source: false
Subtree from ucased: Nodes {
  Data {2,3,4},
  Data {3,4,5},
  Data {4,5,6},
};
Subtree from cow: Nodes {
  Data {2,3,4},
  Data {3,4,5},
  Data {4,5,6},
};
Subtrees match: true
Subtrees shared: true
Siblings unshared: true
Parents unshared: true
Roots unshared: true

【讨论】:

    【解决方案2】:

    解决必须在unique_ptr&lt;T&gt; 上调度问题的一种方法是回到在访问者中传递指针的方式,但将返回类型更改为Node*,如下所示:

    // Top-level function stays the same
    Node* genericVisit(Node* node) {
        if (dynamic_cast<Subnode2>(node)) {
            return visit(static_cast<Subnode2*>(node));
        }
        // etc
    }
    // Specialized overloads can return replacements as they go
    virtual Node* visit(Subnode2* node) {
        if (mustReplace()) {
            Subnode1 *myReplacement = ...
            return myReplacement;
        }
        if (node->getChild()) {
            Node *replacement = genericVisit(node->getChild());
            // nullptr means no replacement; non-null means we replace the child
            if (replacement) {
                node->setChild(replacement);
            }
        }
        return nullptr;
    }
    

    此方法要求实施者在执行替换之前注意返回的内容(即nullptr 与否)。另一方面,它将执行替换的最终决定权掌握在进行访问者调用的函数手中,这最终转化为对内部的更多控制,即更好的封装。

    【讨论】:

    • 这可以工作,我必须尝试它在我的设计中是如何工作的。我发布的示例是一个简化版本,因为实际上重载访问会执行更多操作(例如,enteringNode(node); for (const auto* n : node-&gt;getElements()) genericVisit(n); exitingNode(node);enteringNodeexitingNode 都有每个节点类型的自定义行为。可能只让 exitingNode转换孩子并返回新创建的孩子,但这需要大量的强制转换和代码重复。
    • @Jack 您应该能够使用一个简单的模板函数处理强制转换和重新分配,该函数采用指针T* 和对unique_ptr&lt;U&gt;&amp; 的引用,如果dynamic_cast&lt;U*&gt;(t) 成功则执行条件替换.
    • 我设法实现了它,它似乎有效,我只是用一个简单的案例进行了测试,随着时间的推移我会尝试更复杂的情况。最后我用了一个简单的模板方法:gist.github.com/Jakz/e8166edf453c6f2c9630
    猜你喜欢
    • 2016-03-14
    • 1970-01-01
    • 2014-02-04
    • 2015-06-17
    • 2021-12-20
    • 1970-01-01
    • 2020-02-05
    • 1970-01-01
    • 2021-12-04
    相关资源
    最近更新 更多