2

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).

Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
Pablo
  • 557
  • 3
  • 16
  • Do you just want to avoid adding another `if (index == 4) return transit_to<4>();` line to `transit_to(std::size_t)`? – paolo Jul 01 '22 at 08:55
  • @paolo Yes, that's the point: Avoiding adding a new line everytime a new state is added – Pablo Jul 01 '22 at 08:56
  • @Pepijn Kramer Yes you are right and real implementation works as you say. This is an isolated minimun example to avoid struggle with much bigger source code, just to show a problem. So please, forget about FSM, it was just a comment to set context to my question – Pablo Jul 01 '22 at 08:59
  • @hyde Yes, you are right. I would edit it with your suggestion. Thanks – Pablo Jul 01 '22 at 09:01
  • Does this answer your question? [Specify template parameters at runtime](https://stackoverflow.com/questions/2873802/specify-template-parameters-at-runtime) – Nimrod Jul 01 '22 at 09:04

3 Answers3

5

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
}
paolo
  • 2,345
  • 1
  • 3
  • 17
  • Although not tagged C++17, OP mentioned: _"Use C++17 or below (sorry, no c++20 fancy features please)"_ Should be a quick fix though. – Ted Lyngmo Jul 01 '22 at 09:13
  • This compiles in C++17, doesn't it? – paolo Jul 01 '22 at 09:15
  • 2
    No, but [this version of your idea does](https://godbolt.org/z/jf9qjK1fq). Very nice solution. +1 – Ted Lyngmo Jul 01 '22 at 09:16
  • 1
    @TedLyngmo Thanks a lot. I've updated my answer. For some reason, I was convinced that lambda templates where available since C++17. – paolo Jul 01 '22 at 09:22
  • I hope you don't mind but I used your idea to populate the map in my answer. – Ted Lyngmo Jul 01 '22 at 09:48
  • 1
    @TedLyngmo I don't: I'm glad it helped you. You just spelled my name wrong (no worries: "paolo" -> "paulo" is a common typo ;) ) – paolo Jul 01 '22 at 10:12
  • Oups, sorry! I blame my bad eyes (and that my second name is _Paul_) :-) – Ted Lyngmo Jul 01 '22 at 10:15
  • 1
    @paola Thanks to you both, Paolo and Ted, the solution Paolo offers is really great, and Ted's C++17 conversion has been really helpful to me. So again, great job nd thank you! – Pablo Jul 01 '22 at 11:12
1

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();
}
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • but then the whole tuple can be removed and the whole map is just a runtime map from index to state which would simplify things even a bit more – Pepijn Kramer Jul 01 '22 at 09:03
  • @PepijnKramer Possibly. I assume that once the template "domain" has been entered, there are things OP would prefer to be done during compile time. – Ted Lyngmo Jul 01 '22 at 09:05
1

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

Marek R
  • 32,568
  • 6
  • 55
  • 140