ppforest2 uses the Visitor pattern (double dispatch) to traverse trees and models without dynamic_cast. You can add new traversal logic — statistics, export formats, analysis — by implementing a visitor, without modifying the model types.
Two visitor interfaces are available:
| Interface | Dispatches over | Use when |
| TreeNode::Visitor | TreeBranch (split), TreeLeaf (leaf) | You need to walk the tree structure |
| Model::Visitor | Tree, Forest | You need to handle trees and forests differently |
How double dispatch works
Each visitable class has an accept() method that calls the appropriate visit() overload on the visitor:
node->accept(visitor);
void TreeBranch::accept(TreeNode::Visitor& v) const { v.visit(*this); }
void TreeLeaf::accept(TreeNode::Visitor& v) const { v.visit(*this); }
void MyVisitor::visit(const TreeBranch& cond) { ... }
void MyVisitor::visit(const TreeLeaf& resp) { ... }
This resolves both the node type and the visitor type at runtime without any casts.
Implementing a TreeNode::Visitor
Interface to implement
class TreeNode::Visitor {
public:
virtual ~Visitor() = default;
virtual void visit(const TreeBranch& condition) = 0;
virtual void visit(const TreeLeaf& response) = 0;
};
TreeBranch (internal split node) provides:
projector — the projection vector (p) used at this split
cutpoint — the split cutpoint in projected space
lower / upper — child nodes (TreeNode::Ptr)
groups — set of group labels reachable from this node
pp_index_value — the PP index value achieved at this split
training_spec — the TrainingSpec used to build this subtree
TreeLeaf (leaf node) provides:
value — the predicted group label
Traversal pattern
To walk the full tree, recursively call accept() on child nodes inside your visit(TreeBranch&) method:
void visit(const TreeBranch& cond) override {
cond.lower->accept(*this);
cond.upper->accept(*this);
}
void visit(const TreeLeaf& resp) override {
}
Example: computing tree depth
class DepthVisitor : public TreeNode::Visitor {
public:
int max_depth = 0;
int current_depth = 0;
void visit(const TreeBranch& cond) override {
++current_depth;
if (current_depth > max_depth)
max_depth = current_depth;
cond.lower->accept(*this);
cond.upper->accept(*this);
--current_depth;
}
void visit(const TreeLeaf&) override {
++current_depth;
if (current_depth > max_depth)
max_depth = current_depth;
--current_depth;
}
};
}
Binarization strategies for multiclass-to-binary reduction.
Definition Benchmark.hpp:25
Usage:
DepthVisitor visitor;
tree.root->accept(visitor);
int depth = visitor.max_depth;
Example: counting leaves
class LeafCountVisitor : public TreeNode::Visitor {
public:
int count = 0;
void visit(const TreeBranch& cond) override {
cond.lower->accept(*this);
cond.upper->accept(*this);
}
void visit(const TreeLeaf&) override {
++count;
}
};
Usage:
LeafCountVisitor visitor;
tree.root->accept(visitor);
int leaves = visitor.count;
Implementing a Model::Visitor
Model::Visitor dispatches over Tree and Forest, useful when you need to handle them differently (e.g. serialisation, summary statistics).
Interface to implement
class Model::Visitor {
public:
virtual ~Visitor() = default;
virtual void visit(const Tree& tree) = 0;
virtual void visit(const Forest& forest) = 0;
};
Example: collecting all leaf labels
class AllLabelsVisitor : public Model::Visitor {
public:
std::set<types::Outcome> labels;
void visit(const Tree& tree) override {
class LabelCollector : public TreeNode::Visitor {
public:
std::set<types::Outcome>& labels;
LabelCollector(std::set<types::Outcome>& l) : labels(l) {}
void visit(const TreeBranch& c) override {
c.lower->accept(*this);
c.upper->accept(*this);
}
void visit(const TreeLeaf& r) override {
labels.insert(r.value);
}
};
LabelCollector collector(labels);
tree.root->accept(collector);
}
void visit(const Forest& forest) override {
for (const auto& bt : forest.trees)
visit(static_cast<const Tree&>(*bt));
}
};
Usage:
AllLabelsVisitor visitor;
model.accept(visitor);
Existing visitors as reference
Conventions
- Single-use — instantiate a fresh visitor for each traversal. Do not reuse a visitor across multiple trees without resetting its state.
- Mutable accumulators — store results in public mutable members (e.g.
int count, json result, std::vector<NodeData> nodes). Read them after accept() returns.
- Not thread-safe — visitors carry mutable state and should not be shared across threads. Use one visitor per thread if needed.
- Recursive traversal — call
cond.lower->accept(*this) and cond.upper->accept(*this) inside visit(TreeBranch&) to walk the full tree. Omit these calls if you only need to inspect the root.
- See also
- TreeNode::Visitor, Model::Visitor, VIVisitor, TreeBranch, TreeLeaf