From boiler plate code to template implementation

Viewed 73

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:

  1. Use C++17 or below (sorry, no c++20 fancy features please).

  2. Do not change interface (do not propose accessing by type or so on things).

3 Answers

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
}

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();
}

I think put pointers to functions in array can make this nice clean and fast. It is even agnostic to fact that there is a std::tuple.

using States = std::tuple<state1, state2, state3, state4>;

template<typename State>
void transit_to()
{
    std::cout << __PRETTY_FUNCTION__ << '\n';
}

template<typename>
struct state_array;

template<template<typename...> class States, typename...Ts>
struct state_array<States<Ts...>>
{
    static constexpr std::array transitions { &transit_to<Ts>... };
};


void transit_to(size_t index)
{
    state_array<States>::transitions[index]();
}

https://godbolt.org/z/Exfn8ben1

Related