• Keine Ergebnisse gefunden

classes, soTypeTreeuses tag dispatch. For simplicity, every node mixin explicitly exports its node tag via a public typedef calledNodeTag.

Some algorithms inTypeTree(tree transformations in particular) are additionally controlled by the user type of a node. For consistency and to avoid the fragility of partial template specialization, user types are also classified using a second tag type, which the user has to export under the name ImplementationTag. Taken together, these two tags make it possible to have completely different user payloads that share a common TypeTree node type (for example, PDELab has aPowerGridFunctionSpaceand a VectorGridFunctionSpace, both implemented as a PowerNode) as well as the reverse (the same type of user node sitting on top of different tree topologies – this makes it very easy to implement proxy nodes, a feature that is heavily used by Dune-Multidomain for its subproblem function spaces).

5.4 • Algorithms 79

A

A1

A1,1 A1,1

A2

A2,1

A

A1

A1,1 A1,1

A2

A2,1

Breadth-first Traversal Depth-first Traversal

1 2

3

4 5

1

2

3 4

5

6 7

8

9 10

Figure 5.2 — Breadth-first and depth-first tree traversal

Depth-first traversal does instead follow a given path down the tree as far as possible and then continues to search for the next eligible path by means of backtracking. This corresponds to using a stack, where elements are both added to and retrieved from the same end of the data structure.

Figure 5.2 shows the traversal path through an example tree for both breadth-first and depth-first traversal. Interestingly, implementing the latter algorithm for our trees is relatively straightforward, while a breadth-first traversal would be very difficult to realize. This is due to the fact that the traversal algorithm has to be written as a TMP, and the stack-based nature of the depth-first traversal naturally matches the recursive programming style of template meta programming; we can simply use the call stack of our program to store the algorithm state. On the other hand, manually implementing an efficient queue for heterogeneous data using C++

templates seems like a very hard task.

Fortunately, the intended applications in our framework only require depth-first traversal, so we have restricted ourselves to that kind of iteration. If we take a closer look at the algorithm, it becomes clear that due to its backtracking property, the traversal trajectory passes interior tree nodes multiple times. Depending on when the algorithm stops at those nodes, depth-first traversal can be further categorized:

Pre-order traversal visits interior nodes as soon as they are encountered for the first time. It is useful for algorithms that need to propagate information down the tree.

In-order traversal is mostly important for ordered binary trees, where it visits the nodes in the sorting order.

1 2

3 4

5 6

4 2

1 3

6 5

6 3

1 2

5 4

Pre-order Traversal In-order Traversal Post-order Traversal Figure 5.3 — Pre-, in- and post-order depth-first tree traversal

Post-order traversal must be used when operations on interior nodes require the results of all descendants, i.e. information has to be moved up the tree.

Figure 5.3 depicts the order in which the nodes of an example tree are visited for each of the three variants.

In order to separate the iteration algorithm from the operations performed on the tree nodes, computer science has developed the visitor pattern [99, 26]: a generic algorithm traverses the tree and presents individual elements to the visitor by invoking its callback function. In the context of trees, a visitor may have multiple callback functions that are called at different times to implement the different traversal orders. This allows a single visitor to e.g. perform a combined pre- and post-order traversal.. Listing 5.3shows the callback points offered by the visitor interface of TypeTree. In addition to the actual node, the callbacks also receive information about the position of the node within the tree via the additional TreePath parameter, which encodes the path from the root to the current node as a tuple of child indices. While the pre, in and post callbacks are commonly encountered in tree libraries, our interface also contains two additional methods that help in writing algorithms where data has to be moved up or down the tree hierarchy. The design of the data structure makes this difficult to accomplish manually because there is no link from child to parent. Finally, the visitor also contains a template meta function that is called for each node to decide whether or not traversal should continue into the children of that node (in the example above, that function is inherited from the base class). This is an important optimization, as the tree traversal is a TMP itself and thus completely unrolled; unnecessarily traversing a large number of deep tree hierarchies would greatly increase compile times of TypeTree-based programs.

The actual traveral Algorithm5.1is based on a modular framework based on compile time dispatch to node-specific iteration logic. Depending on the characteristics of the current node, this logic may employ run time iteration (which has the advantage of reducing code size and compile time, but is only possible for homogeneously structured nodes like PowerNode) or compile time recursion. At the same time, this modular approaches ensures easy extensibility, as new node types can be

5.4 • Algorithms 81

Listing 5.3 — TypeTree visitor interface

1 struct Visitor

2 : public TypeTree::DefaultVisitor

3 {

4 // pre-order callback, called for interior nodes 5 template<typename Node, typename TreePath>

6 void pre(Node&& node, TreePath tree_path)

7 {}

8

9 // in-order callback, called for interior nodes 10 template<typename Node, typename TreePath>

11 void in(Node&& node, TreePath tree_path)

12 {}

13

14 // post-order callback, called for interior nodes 15 template<typename Node, typename TreePath>

16 void post(Node&& node, TreePath tree_path)

17 {}

18

19 // callback for leaf functions

20 template<typename Node, typename TreePath>

21 void leaf(Node&& node, TreePath tree_path)

22 {}

23

24 // called before traversing down a father-child relationship with information 25 // about both father and child; simplifies data propagation down the tree 26 template<typename Node, typename Child, typename TreePath, typename ChildIdx>

27 void beforeChild(Node&& node, Child&& child, TreePath tp, ChildIdx ci)

28 {}

29

30 // called before traversing up a child-father relationship with information 31 // about both father and child; simplifies data propagation up the tree 32 template<typename Node, typename Child, typename TreePath, typename ChildIdx>

33 void afterChild(Node&& node, Child&& child, TreePath tp, ChildIdx ci)

34 {}

35 };

accommodated by adding a new dispatch overload. Importantly, adding this overload does not require any changes to existing algorithm components.

5.4.2 Simultaneous Traversal of Tree Pairs

Within the context of PDELab, there is a recurring need to traverse a tree and apply a function that needs data which is stored in a different tree, e.g. the data required to interpolate a function or evaluate the constraints (cf. Section 2.3.3).

Essentially, we need to traverse two trees in parallel and present the visitor with matching pairs of tree nodes. TypeTreecontains an extended iteration algorithm that enables this usage scenario.

Algorithm 5.1 — Tree traversal algorithm. N is the current tree node, V the visitor, and pthe tree path. In this algorithm, we show a single apply() function with its general semantics. The dispatch stage uses the component registry D to look up a version of apply() that is tailored to the node type of the current node N and implements the apply functionality in a way that is optimized for the data layout of the node.

function dispatch(N, V, p) f ← D[tag(N)]

f(N, V, p)

function apply(N, V, p= ()) if V.wantsToVisit(N)then

ifN is leaf node then V.leaf(N, p) else

V.pre(N, p)

n← |children(N)| for i←1, n do

C ←children(N)[i]

qpk(i)

V.beforeChild(N, C, p, i)

dispatch(C, V, q) .dispatch to apply function for child V.afterChild(N, C, p, i)

if i < n then V.in(N, p) V.post(N, p)

In general, it is desirable not to require the two trees to be structured identically, as long as they are compatible in the sense that corresponding nodes in the two trees have either an identical number of children, or at least one of the nodes does not have any children at all. With this relaxed requirement, it becomes possible to iterate over tree pairs where the two trees employ different node types for identically positioned nodes and to support scenarios where one tree is cut off at some interior nodes. If we encounter such a cut-off scenario, it is no longer obvious how the traversal should proceed: We can continue traversing the “deeper”

tree and call the visitor with all resulting combinations of the additional nodes in that tree and the single node in the other tree, which leaves us with the question of how to distinguish this special case in the visitor. For that reason, we have instead opted to simply ignore the additional tree nodes in the deeper tree and to invoke the leaf callback on the visitor with the two corresponding nodes that introduced the discrepancy. If a visitor needs to continue the traversal, it is trivial