I am implementing a finite state machine where all possible states are stored within a std::tuple
.
This is a minimum compiling example of the problem I am facing and its godbolt link https://godbolt.org/z/7ToKc3T3W:
#include <tuple>
#include <stdio.h>
struct state1 {};
struct state2 {};
struct state3 {};
struct state4 {};
std::tuple<state1, state2, state3, state4> states;
template<size_t Index>
void transit_to()
{
auto state = std::get<Index>(states);
//Do some other actions over state....
}
void transit_to(size_t index)
{
if (index == 0) return transit_to<0>();
if (index == 1) return transit_to<1>();
if (index == 2) return transit_to<2>();
if (index == 3) return transit_to<3>();
}
int main()
{
for(int i=0; i<=3; i)
transit_to(i);
}
In my case, I would like to change the void transit_to(size_t index)
implementation to some template construction where all the boiler plate code can be simplified in case I add new states during development.
The only constraints are:
Use C 17 or below (sorry, no c 20 fancy features please).
Do not change interface (do not propose accessing by type or so on things).
CodePudding user response:
If you only want to avoid adding another line like
if (index == 4) return transit_to<4>();
when you add a new state, e.g. struct state5
, you may implement transit_to(std::size_t)
like this:
// Helper function: Change state if I == idx.
// Return true, if the state was changed.
template <std::size_t I>
bool transit_if_idx(std::size_t idx) {
bool ok{false};
if (idx == I) {
transit_to<I>();
ok = true;
}
return ok;
}
template <std::size_t... Is>
bool transit_to_impl(std::size_t idx, std::index_sequence<Is...>) {
return (transit_if_idx<Is>(idx) || ...);
}
void transit_to(std::size_t index) {
constexpr static auto tupleSize = std::tuple_size_v<decltype(states)>;
[[maybe_unused]] auto const indexValid =
transit_to_impl(index, std::make_index_sequence<tupleSize>{});
assert(indexValid); // Check if index actually referred to a valid state
}
CodePudding user response:
I don't see how you could get away from the translation from a runtime value to a specific instance of a function template, but you could make a map to simplify it:
#include <unordered_map>
void transit_to(size_t index) {
static const std::unordered_map<size_t, void (*)()> tmap{
{0, transit_to<0>},
{1, transit_to<1>},
{2, transit_to<2>},
{3, transit_to<3>},
};
if (auto it = tmap.find(index); it != tmap.end()) it->second();
}
Building on paolo's nice answer, you could avoid having to populate the map manually:
#include <unordered_map>
std::tuple<state1, state2, state3, state4> states;
template <size_t Index>
void transit_to() {
auto state = std::get<Index>(states);
}
template<std::size_t... I>
void populate_map(std::unordered_map<size_t, void(*)()>& m,
std::index_sequence<I...>) {
((m[I] = transit_to<I>), ...);
};
void transit_to(size_t index) {
static const auto tmap = []{
std::unordered_map<size_t, void(*)()> rv;
populate_map(rv, std::make_index_sequence<
std::tuple_size_v<decltype(states)>>{});
return rv;
}();
if (auto it = tmap.find(index); it != tmap.end()) it->second();
}