我有一个异构的n-ary树,由不同类型的节点组成,例如:
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<Subnode2>&
,以便我访问唯一指针,并且我可以在访问时更改智能指针的内容。这可行,但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)我没有看到任何问题。
答案 0 :(得分:4)
因为我认为评论不足以传达所有信息:
@Jack你可以(Sean Parent: should)在那里使用
shared_pointer<const Node>
,并根据需要复制更改。胜利的不可能性!也就是说,也许你可以完全没有继承?使事情更简单,更有效 - 见6 hours ago
我以为我会以相反的顺序展示这两个方面:
对树使用静态多态性:让我们定义一个树,其中叶节点可以是字符串或用户定义的结构:
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}
- &gt; 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))
)现在您会注意到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,并观察另一个也发生更改)
现在采用Sean Parent方法:使用shared_ptr<T const>
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);
}
正如您所看到的,转换是完全通用的,并且与SharedTree
以及CowTree
一起使用 - 因为没有任何值会发生变异。显然,使用CowTree
更加安全,因为它可以保证不变性。
测试程序主动检查
cow
树在我们获得ucased
转换时不会发生变化。<强> 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
答案 1 :(得分:1)
解决必须在unique_ptr<T>
上发送问题的一种方法是返回到在访问者中传递指针的方式,但将返回类型更改为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
或不是{{1}})。另一方面,它最终决定在访问者调用的函数中执行替换,这最终转化为对内部的更多控制,即更好的封装。