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.
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));
}
};
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/...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With