Traversing trees at compile time with C++17 Variadic Templates
Asked Answered
D

2

10

I'm currently looking into using C++ (C++17) variadic templates for generating efficient, real-time simulations of circuits.

My goal is to leverage variadic templates to define a tree that can be traversed at compile-time. To define such a tree, I use the following three structs:

template <auto Tag> struct Leaf
{
    static constexpr auto tag = Tag;
};

template <typename ... Children> struct Branch
{
    static constexpr auto child_count = sizeof ... (Children);
    
    template <typename Lambda> constexpr void for_each_child(Lambda && lambda)
    {
        // TODO: Execute 'lambda' on each child.
    }
    
    std::tuple<Children ...> m_children {};
};

template <typename Root> struct Tree
{
    template <auto Tag> constexpr auto & get_leaf()
    {
        // TODO: Traverse the tree and find the leaf with tag 'Tag'.
        
        // If there's no leaf with tag 'Tag' the program shouldn't compile.
    }
    
    Root root {};
};

Using the above definition of a tree, we can define a set of circuit components as follows:

template <auto Tag> struct Resistor : Leaf<Tag>
{
    float resistance() { return m_resistance; }
    
    float m_resistance {};
};

template <auto Tag> struct Capacitor : Leaf<Tag>
{
    float resistance() { return 0.0f; }
    
    float m_capacitance {};
};

template <typename ... Children> struct Series : Branch<Children ...>
{
    using Branch<Children ...>::for_each_child;
    
    float resistance()
    {
        float acc = 0.0f;
        
        for_each_child([&acc](auto child) { acc += child.resistance(); });
        
        return acc;
    }
};

template <typename ... Children> struct Parallel : Branch<Children ...>
{
    using Branch<Children ...>::for_each_child;
    
    float resistance()
    {
        float acc = 0.0f;
        
        for_each_child([&acc](auto child) { acc += 1.0f / child.resistance(); });
        
        return 1.0f / acc;
    }
};

Next, using the above components, we can express a specific circuit like this:

enum { R0, R1, C0, C1 };

using Circuit =
    Tree<
        Parallel<
            Series<
                Resistor<R0>,
                Capacitor<C0>
            >, // Series
            Series<
                Resistor<R0>,
                Capacitor<C1>
            > // Series
        > // Parallel
    >; // Tree

...where R0, R1, C0, and C1 are tags that we use for accessing components at compile time. E.g. a very basic use case could be the following:

int main()
{
    Circuit circuit {};
    
    circuit.get_leaf<R0>().m_resistance  =  5.0E+3f;
    circuit.get_leaf<C0>().m_capacitance = 10.0E-3f;
    circuit.get_leaf<R1>().m_resistance  =  5.0E+6f;
    circuit.get_leaf<C1>().m_capacitance = 10.0E-6f;
    
    std::cout << circuit.root.resistance() << std::endl;
}

What I just can't wrap my head around is how to implement the functions for_each_child and get_leaf. I've tried different approaches using if-constexpr statements and template-structs without finding a good solution. Variadic templates are interesting but daunting at the same time. Any help would be greatly appreciated.

Demarcate answered 24/6, 2021 at 12:26 Comment(3)
Good point - expression templates look interesting. The reason I need to use a tree, as described above, is that I need to separate the state of tree from the actual tree. Doing so makes it possible to store the tree state globally and then create and actual tree instance whenever I need to modify the tree state (and dispose the tree afterwards -- while only keeping the tree state). The C++ optimizer will optimise all of the bookkeeping that the tree instance does and what you end up with is code that is almost as efficient as hand-optimised C-code. I've tested it and it works like a bliss.Demarcate
Is any circuit a tree? How would you represent a rectangular grid of elements?Cowbind
We use Wave Digital Filters to simulate the circuit, which represent a circuit as a binary connection tree. ccrma.stanford.edu/~jos/pasp/Wave_Digital_Filters_I.html, ccrma.stanford.edu/~jos/WaveDigitalFilters/…Demarcate
D
3

After studying various articles on C++ Variadic Templates, I've managed to patch up a solution to the problem.

First, to implement for_each_child we use the following helper function that works as a for-loop that is un-rolled at compile-time:

template <auto from, auto to, typename Lambda>
    static inline constexpr void for_constexpr(Lambda && lambda)
{
    if constexpr (from < to)
    {
        constexpr auto i = std::integral_constant<decltype(from), from>();
        
        lambda(i);
        
        for_constexpr<from + 1, to>(lambda);
    }
}

By using this helper function we can implement for_each_child as follows:

template <typename ... Children> struct Branch
{
    static constexpr auto children_count = sizeof ... (Children);

    template <typename Lambda> constexpr void for_each_child(Lambda && lambda)
    {
        for_constexpr<0, children_count>([lambda, this](auto i)
        {
            lambda(std::get<i>(m_children));
        });
    }
    
    std::tuple<Children ...> m_children {};
};

Next, to implement get_leaf, we use a bunch of different helper functions. As Caleth suggested, we can divide the problem into two steps. First, we compute the path from the root to the desired leaf; afterwards, we can follow that path to extract the leaf from the tree.

A path can be represented as an index sequence as follows:

template <auto ...indices> using Path = std::index_sequence<indices...>;

The first helper function we need checks whether a node has a leaf with a given tag:

template <auto tag, class Node> struct has_path
{
    static constexpr
        std::true_type
            match(const Leaf<tag>);
    
    template <class ...Children> static constexpr
        typename std::enable_if<
            (has_path<tag, Children>::type::value || ...),
            std::true_type
        >::type
            match(const Branch<Children...>);
    
    static constexpr
        std::false_type
            match(...);
    
    using type = decltype(match(std::declval<Node>()));
};

We simply pattern match on the node. If it is a leaf we must make sure that it has the correct tag. And, if it is a branch, we need to ensure that one of the children has a leaf with the tag.

The next helper function is a bit more complicated:

template <auto tag, class Node, auto ...indices> struct find_path
{
    template <auto index, class Child, class ...Children> struct search_children
    {
        static constexpr auto fold()
        {
            if constexpr(has_path<tag, Child>::type::value)
            {
                return typename find_path<tag, Child, indices..., index>::type();
            }
            else
            {
                return typename search_children<index + 1, Children...>::type();
            }
        }
        
        using type = decltype(fold());
    };
    
    static constexpr
        Path<indices...>
            match(const Leaf<tag>);
    
    template <class ...Children> static constexpr
        typename search_children<0, Children...>::type
            match(const Branch<Children...>);
    
    using type = decltype(match(std::declval<Node>()));
};

We accumulate the path in the indices template parameter. If the node that we are investigating (via the template parameter Node) is a leaf, we check that it has the correct tag and, if so, return the accumulated path. If instead, the node is a branch we have to use the helper function search_children which iterates through all the children in the branch. For each child, we first check whether that child has a leaf with the given tag. If so, we append the current index (given by the template parameter index) to the accumulated path and call find_path recursively on that child. If the child does not have a leaf with the given tag, we try the next child instead, and so on.

Finally, we define a helper function that can extract a leaf given a path:

template <class Node>
    static inline constexpr auto &
        get(Node & leaf, Path<> path)
{
    return leaf;
}

template <auto index, auto ...indices, class Node>
    static inline constexpr auto &
        get(Node & branch, Path<index, indices...> path)
{
    auto & child = std::get<index>(branch.m_children);
    
    return get(child, Path<indices...>());
}

Using find_path and get we can implement get_leaf as follows:

template <typename Root> struct Tree
{
    template <auto tag> constexpr auto & get_leaf()
    {
        constexpr auto path = typename implementation::find_path<tag, Root>::type {};
        
        return implementation::get(root, path);
    }
    
    Root root;
};

Here's a link to godbolt.org that demonstrates that the code compiles and works as expected with Clang:

godbolt.org/...

Demarcate answered 4/7, 2021 at 22:13 Comment(0)
G
10

for_each_child is fairly simple with std::index_sequence.

template <typename ... Children> struct Branch
{
    using indexes = std::index_sequence_for<Children...>;
    static constexpr auto child_count = sizeof... (Children);
    
    template <typename Lambda> constexpr void for_each_child(Lambda && lambda)
    {
        for_each_child_impl(std::forward<Lambda>(lambda), indexes{});
    }
    
    std::tuple<Children ...> m_children {};

private:
    template <typename Lambda, std::size_t... Is> constexpr void for_each_child_impl(Lambda && lambda, std::index_sequence<Is...>)
    {
        (lambda(std::get<Is>(m_children)), ...);
    }
};

get_leaf is slightly trickier. First we work out what the path is to the desired leaf, then we follow the path from root.

template <std::size_t I, typename>
struct index_sequence_cat;

template <std::size_t I, std::size_t... Is>
struct index_sequence_cat<I, std::index_sequence<Is...>> {
    using type = std::index_sequence<I, Is...>;
};

template <std::size_t I, typename Ix>
using index_sequence_cat_t = typename index_sequence_cat<I, Ix>::type;

template<typename, auto Tag, typename, std::size_t... Is> 
struct leaf_index {};

template<auto Tag, typename T, std::size_t... Is> 
using leaf_index_i = typename leaf_index<void, Tag, T, Is...>::index;

template<auto Tag, std::size_t I> 
struct leaf_index<void, Tag, Leaf<Tag>, I> {
    using index = std::index_sequence<I>;
};

template<typename, auto, std::size_t, typename...>
struct branch_index {};

template<auto Tag, std::size_t I, typename... Args>
using branch_index_i = typename branch_index<void, Tag, I, Args...>::index;

template<auto Tag, std::size_t I, typename First, typename... Args>
struct branch_index<std::void_t<leaf_index_i<Tag, First, I>>, Tag, I, First, Args...> {
    using index = leaf_index_i<Tag, First, I>;
};

template<auto Tag, std::size_t I, typename First, typename... Args>
struct branch_index<std::void_t<branch_index_i<Tag, I + 1, Args...>>, Tag, I, First, Args...> {
    using index = branch_index_i<Tag, I + 1, Args...>;
};

template<auto Tag, typename... Children, std::size_t I> 
struct leaf_index<void, Tag, Branch<Children...>, I> {
    using index = index_sequence_cat_t<I, branch_index_i<Tag, 0, Children...>>;
};

template<auto Tag, typename... Children> 
struct leaf_index<std::void_t<branch_index_i<Tag, 0, Children...>>, Tag, Branch<Children...>> {
    using index = branch_index_i<Tag, 0, Children...>;
};

template <typename Root> struct Tree
{
    template <auto Tag> constexpr auto & get_leaf()
    {
        return get_leaf(leaf_index<Tag, root>{});
    }
    
    Root root {};
private:
    template <std::size_t... Is>
    auto & get_leaf(std::index_sequence<Is...>)
    {
        return get_leaf<Is...>(root);
    }

    template<std::size_t I, typename T>
    auto& get_leaf(T & branch)
    {
        return std::get<I>(branch.m_children);
    }
    
    template<std::size_t I, std::size_t J, std::size_t... Is, typename T>
    auto& get_leaf(T & branch)
    {
        return get_leaf<J, Is...>(std::get<I>(branch.m_children));
    }
};
Godoy answered 24/6, 2021 at 14:28 Comment(1)
Hi Caleth, thanks for helping out with solving the problem. Your code still doesn't compile in neithor Clang nor GCC, which is why I decided to share my own solution and mark that as the accepted answer.Demarcate
D
3

After studying various articles on C++ Variadic Templates, I've managed to patch up a solution to the problem.

First, to implement for_each_child we use the following helper function that works as a for-loop that is un-rolled at compile-time:

template <auto from, auto to, typename Lambda>
    static inline constexpr void for_constexpr(Lambda && lambda)
{
    if constexpr (from < to)
    {
        constexpr auto i = std::integral_constant<decltype(from), from>();
        
        lambda(i);
        
        for_constexpr<from + 1, to>(lambda);
    }
}

By using this helper function we can implement for_each_child as follows:

template <typename ... Children> struct Branch
{
    static constexpr auto children_count = sizeof ... (Children);

    template <typename Lambda> constexpr void for_each_child(Lambda && lambda)
    {
        for_constexpr<0, children_count>([lambda, this](auto i)
        {
            lambda(std::get<i>(m_children));
        });
    }
    
    std::tuple<Children ...> m_children {};
};

Next, to implement get_leaf, we use a bunch of different helper functions. As Caleth suggested, we can divide the problem into two steps. First, we compute the path from the root to the desired leaf; afterwards, we can follow that path to extract the leaf from the tree.

A path can be represented as an index sequence as follows:

template <auto ...indices> using Path = std::index_sequence<indices...>;

The first helper function we need checks whether a node has a leaf with a given tag:

template <auto tag, class Node> struct has_path
{
    static constexpr
        std::true_type
            match(const Leaf<tag>);
    
    template <class ...Children> static constexpr
        typename std::enable_if<
            (has_path<tag, Children>::type::value || ...),
            std::true_type
        >::type
            match(const Branch<Children...>);
    
    static constexpr
        std::false_type
            match(...);
    
    using type = decltype(match(std::declval<Node>()));
};

We simply pattern match on the node. If it is a leaf we must make sure that it has the correct tag. And, if it is a branch, we need to ensure that one of the children has a leaf with the tag.

The next helper function is a bit more complicated:

template <auto tag, class Node, auto ...indices> struct find_path
{
    template <auto index, class Child, class ...Children> struct search_children
    {
        static constexpr auto fold()
        {
            if constexpr(has_path<tag, Child>::type::value)
            {
                return typename find_path<tag, Child, indices..., index>::type();
            }
            else
            {
                return typename search_children<index + 1, Children...>::type();
            }
        }
        
        using type = decltype(fold());
    };
    
    static constexpr
        Path<indices...>
            match(const Leaf<tag>);
    
    template <class ...Children> static constexpr
        typename search_children<0, Children...>::type
            match(const Branch<Children...>);
    
    using type = decltype(match(std::declval<Node>()));
};

We accumulate the path in the indices template parameter. If the node that we are investigating (via the template parameter Node) is a leaf, we check that it has the correct tag and, if so, return the accumulated path. If instead, the node is a branch we have to use the helper function search_children which iterates through all the children in the branch. For each child, we first check whether that child has a leaf with the given tag. If so, we append the current index (given by the template parameter index) to the accumulated path and call find_path recursively on that child. If the child does not have a leaf with the given tag, we try the next child instead, and so on.

Finally, we define a helper function that can extract a leaf given a path:

template <class Node>
    static inline constexpr auto &
        get(Node & leaf, Path<> path)
{
    return leaf;
}

template <auto index, auto ...indices, class Node>
    static inline constexpr auto &
        get(Node & branch, Path<index, indices...> path)
{
    auto & child = std::get<index>(branch.m_children);
    
    return get(child, Path<indices...>());
}

Using find_path and get we can implement get_leaf as follows:

template <typename Root> struct Tree
{
    template <auto tag> constexpr auto & get_leaf()
    {
        constexpr auto path = typename implementation::find_path<tag, Root>::type {};
        
        return implementation::get(root, path);
    }
    
    Root root;
};

Here's a link to godbolt.org that demonstrates that the code compiles and works as expected with Clang:

godbolt.org/...

Demarcate answered 4/7, 2021 at 22:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.