-1

I'm relatively new to c++, mostly worked with python.

I have a scenario where a user(me) uses a GUI to send commands to a microcontroller via serial, and then the microcontroller processes them.

Right now i have 10 commands, but as the project develops (some form of modular robot) I can envision having 50-100 possible commands.

Is there a better way for my c++ handleCommands function to select which one of the possible 100 functions to run without doing massive case switches or if else statements?

Extract of the code:

char cmd = 1; // example place holder
int value = 10; //example place holder
switch (cmd){
case '1':
  toggleBlink(value);
  break;
case '2':
  getID(value); // in this case value gets ignored by the function as its not required
  break;

This works fine for 3-4 functions but doesn't seem to me like the best way to do it for more functions.

I've heard of lookup tables but as each function is different and may require arguments or not I'm consumed on how to implement them.

Some background on the set-up:

The commands are mainly diagnostic ,< ID > ect and a couple of functional ones that require parameters like, <blink,10> <runto,90> <set-mode,locked>

The validation is done in python against a csv file and the actual serial message sent to the microcontroller is sent as <(index of comand in csvfile),parameter> with < > and , being delimiters.

So the user would type blink,10 and the python app will send <1,10> over serial as blink is found at index 1 of the csv file.

The microcontroller reads these in and i am left over with 2 char arrays, the command array containing a number, and the value array containing the value sent.(also a number)

As I'm running this on a microcontroller i don't really want to have to store a long file of possible commands in flash, hence the validation done on the python gui side.

Note that in the case of a possible multi argument function, say <move,90,30> i.e move 90 degrees in 30 seconds eat, the actual function would only receive one argument "30,90" and then split that up as needed.

Wscott
  • 3
  • 3
  • 2
    One option is an array or `std::unordered_map` full of pointers to functions that do the grunt work previously done in the `switch`. `std::function` can be used in place of the function pointers to hide differences in parameters. – user4581301 Mar 16 '22 at 20:59
  • 1
    You could use `std::map` or a lookup table of ``. The table is data driven, so the code that accesses the table only needs to be written and tested once. Changes can be made to the table without affecting the executable code. Also, tables can be placed into Read-Only-Memory, like Flash. – Thomas Matthews Mar 16 '22 at 21:01
  • @user4581301, how does `std::function` differ from a function pointer when it comes to differences in parameters? – Fatih BAKIR Mar 16 '22 at 21:05
  • 1
    @FatihBAKIR An `std::function` can store a closure which can capture extra parameters or supply missing parameters. But depending on the nature of these parameters, this solution may imply that the map has to be reconstructed for each lookup. For example you can wrap this closure in a `std::function` yet it calls a function with two arguments : `[extra_arg](int value) { some_function(value, extra_arg); }` – François Andrieux Mar 16 '22 at 21:07
  • does `char cmd = 1;` refer to `char cmd = '1';`? – QWERTYL Mar 16 '22 at 21:16
  • @QWERTYL yes sorry i coped the code over wrong – Wscott Mar 16 '22 at 21:19
  • @Wscott *I'm relatively new to c++, mostly worked with python* -- How would you solve this with python? Wouldn't it look [something like the highest rated answer here](https://stackoverflow.com/questions/2283210/python-function-pointer)? If so, C++ has `std::map`, similar in purpose to a dictionary. Given that, the question you may want to pursue is "how do I get std::map to do something similar?" – PaulMcKenzie Mar 16 '22 at 21:19
  • Sometimes it's best to generalize the interface and move the the gathering of the extra information needed by the handler function into the handler function. If that can't work, load a generic container object up with the necessary parameters and pass it in to a function with a general interface. – user4581301 Mar 16 '22 at 21:22
  • Note that while it's a good idea here, often writing a solution in C++ the way you'd write it in Python is counter-productive. Often it's only experience that will tell you whether or not the same approach is optimal in both languages. – user4581301 Mar 16 '22 at 21:25
  • 1
    What is the target platform (speed and memory resources)? I ask because a lot of advice suggesting `std` container classes for example might be rather ill-advised on many embedded systems. While you ca use pretty much all of C on any embedded system, C++ is a much bigger tool bag and not all the sharp and heavy tools are suited to all types of system - you would generally use a subset – Clifford Mar 16 '22 at 21:41
  • 1
    You were probably wrong to change the switch to literal characters. You states that the first argument is a function index, and you want more that 10 functions, so using an integer index was appropriate. Converting the received string to an integer is trivial. – Clifford Mar 16 '22 at 22:16

3 Answers3

1

If you have the commands comming over the serial line in the format

<command-mapped-to-a-number,...comma-separated-parameters...>

we can simulate that like so:

#include <iostream>
#include <sstream>       // needed for simple parsing
#include <string>
#include <unordered_map> // needed for mapping of commands to functors

int main() {
    std::cout << std::boolalpha;

    // example commands lines read from serial:
    for (auto& cmdline : {"<1,10>", "<2,10,90>", "<3,locked>", "<4>"}) {
        std::cout << exec(cmdline) << '\n';
    }
}

exec above is the interpreter that will return true if the command line was parsed and executed ok. In the examples above, command 1 takes one parameter, 2 takes two, 3 takes one (string) and 4 doesn't have a parameter.

The mapping from command-mapped-to-a-number could be an enum:

// uint8_t has room for 256 commands, make it uint16_t to get room for 65536 commands
enum class command_t : uint8_t {
    blink = 1,
    take_two = 2,
    set_mode = 3,
    no_param = 4,
};

and exec would make the most basic validation of the command line (checking < and >) and put it in a std::istringstream for easy extraction of the information on this command line:

bool exec(const std::string& cmdline) {
    if(cmdline.size() < 2 || cmdline.front() != '<' || cmdline.back() != '>' )
        return false;

    // put all but `<` and `>` in an istringstream:
    std::istringstream is(cmdline.substr(1,cmdline.size()-2));

    // extract the command number
    if (int cmd; is >> cmd) {
        // look-up the command number in an `unordered_map` that is mapped to a functor
        // that takes a reference to an `istringstream` as an argument:

        if (auto cit = commands.find(command_t(cmd)); cit != commands.end()) {
            // call the correct functor with the rest of the command line
            // so that it can extract, validate and use the arguments:
            return cit->second(is);
        }
        return false; // command look-up failed
    }
    return false; // command number extraction failed
}

The only tricky part left is the unordered_map of commands and functors. Here's a start:

// a helper to eat commas from the command line
struct comma_eater {} comma;
std::istream& operator>>(std::istream& is, const comma_eater&) {
    // next character must be a comma or else the istream's failbit is set
    if(is.peek() == ',') is.ignore();
    else is.setstate(std::ios::failbit);
    return is;
}

std::unordered_map<command_t, bool (*)(std::istringstream&)> commands{
    {command_t::blink,
     [](std::istringstream& is) {
         if (int i; is >> comma >> i && is.eof()) {
             std::cout << "<blink," << i << "> ";
             return true;
         }
         return false;
     }},
    {command_t::take_two,
     [](std::istringstream& is) {
         if (int a, b; is >> comma >> a >> comma >> b && is.eof()) {
             std::cout << "<take-two," << a << ',' << b << "> ";
             return true;
         }
         return false;
     }},
    {command_t::set_mode,
     [](std::istringstream& is) {
         if (std::string mode; is >> comma && std::getline(is, mode,',') && is.eof()) {
             std::cout << "<set-mode," << mode << "> ";
             return true;
         }
         return false;
     }},
    {command_t::no_param,
     [](std::istringstream& is) {
         if (is.eof()) {
             std::cout << "<no-param> ";
             return true;
         }
         return false;
     }},
};

If you put that together you'll get the below output from the successful parsing (and execution) of all command lines received:

<blink,10> true
<take-two,10,90> true
<set-mode,locked> true
<no-param> true

Here's a live demo.

Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
0

What you are talking about are remote procedure calls. So you need to have some mechanism to serialize and un-serialize the calls.

As mentioned in the comments you can make a map from cmd to the function implementing the command. Or simply an array. But the problem remains that different functions will want different arguments.

So my suggestion would be to add a wrapper function using vardiac templates.

Prefix every command with the length of data for the command so the receiver can read a block of data for the command and knows when to dispatch it to a function. The wrapper then takes the block of data, splits it into the right size for each argument and converts it and then calls the read function.

Now you can make a map or array of those wrapper function, each one bound to one command and the compiler will generate the un-serialize code for you from the types. (You still have to do it once for each type, the compiler only combines those for the full function call).

Goswin von Brederlow
  • 11,875
  • 2
  • 24
  • 42
0

Given an integer index for each "command" a simple function pointer look-up table can be used. For example:

#include <cstdio>

namespace
{
    // Command functions (dummy examples)
    int examleCmdFunctionNoArgs() ;
    int examleCmdFunction1Arg( int arg1 ) ;
    int examleCmdFunction2Args( int arg1, int arg2 ) ;
    int examleCmdFunction3Args( int arg1, int arg2, arg3 ) ;
    int examleCmdFunction4Args( int arg1, int arg2, int arg3, int arg4 ) ;

    const int MAX_ARGS = 4 ;
    const int MAX_CMD_LEN = 32 ;
    typedef int (*tCmdFn)( int, int, int, int ) ;
    
    // Symbol table
    #define CMD( f ) reinterpret_cast<tCmdFn>(f) 
    static const tCmdFn cmd_lookup[] = 
    {
        0, // Invalid command
        CMD( examleCmdFunctionNoArgs ), 
        CMD( examleCmdFunction1Arg ), 
        CMD( examleCmdFunction2Args ), 
        CMD( examleCmdFunction3Args ),
        CMD( examleCmdFunction4Args )
    } ;
}

namespace cmd
{
    // For commands of the form:  "<cmd_index[,arg1[,arg2[,arg3[,arg4]]]]>"
    // i.e an angle bracketed comma-delimited sequence commprising a command  
    //     index followed by zero or morearguments.
    // e.g.:  "<1,123,456,0>"
    int execute( const char* command )
    {
        int ret = 0 ;
        int argv[MAX_ARGS] = {0} ;
        int cmd_index = 0 ;
        int tokens = std::sscanf( "<%d,%d,%d,%d,%d>", command, &cmd_index, &argv[0], &argv[1], &argv[2], &argv[3] ) ;
        
        if( tokens > 0 && cmd_index < sizeof(cmd_lookup) / sizeof(*cmd_lookup) )
        {
            if( cmd_index > 0 )
            { 
                ret = cmd_lookup[cmd_index]( argv[0], argv[1], argv[2], argv[3] ) ;
            }
        }
        
        return ret ;
    }
}

The command execution passes four arguments (you can expand that as necessary) but for command functions taking fewer arguments they will simply be "dummy" arguments that will be ignored.

Your proposed translation to an index is somewhat error prone and maintenance heavy since it requires you to maintain both the PC application symbol table and the embedded look up table in sync. It may not be prohibitive to have the symbol table on the embedded target; for example:

#include <cstdio>
#include <cstring>

namespace
{
    // Command functions (dummy examples)
    int examleCmdFunctionNoArgs() ;
    int examleCmdFunction1Arg( int arg1 ) ;
    int examleCmdFunction2Args( int arg1, int arg2 ) ;
    int examleCmdFunction3Args( int arg1, int arg2, arg3 ) ;
    int examleCmdFunction4Args( int arg1, int arg2, int arg3, int arg4 ) ;

    const int MAX_ARGS = 4 ;
    const int MAX_CMD_LEN = 32 ;
    typedef int (*tCmdFn)( int, int, int, int ) ;
    
    // Symbol table
    #define SYM( c, f ) {#c,  reinterpret_cast<tCmdFn>(f)} 
    static const struct
    {
        const char* symbol ;
        const tCmdFn command ;
        
    } symbol_table[] = 
    {
        SYM( cmd0, examleCmdFunctionNoArgs ), 
        SYM( cmd1, examleCmdFunction1Arg ), 
        SYM( cmd2, examleCmdFunction2Args ), 
        SYM( cmd3, examleCmdFunction3Args ),
        SYM( cmd4, examleCmdFunction4Args )
    } ;
}

namespace cmd
{
    // For commands of the form:  "cmd[ arg1[, arg2[, arg3[, arg4]]]]"
    // i.e a command string followed by zero or more comma-delimited arguments
    // e.g.:  "cmd3 123, 456, 0"
    int execute( const char* command_line )
    {
        int ret = 0 ;
        int argv[MAX_ARGS] = {0} ;
        char cmd[MAX_CMD_LEN + 1] ;
        int tokens = std::sscanf( "%s %d,%d,%d,%d", command_line, cmd, &argv[0], &argv[1], &argv[2], &argv[3] ) ;
        
        if( tokens > 0 )
        {
            bool cmd_found = false ;
            for( int i = 0; 
                 !cmd_found && i < sizeof(symbol_table) / sizeof(*symbol_table);
                 i++ )
            {
                cmd_found = std::strcmp( cmd, symbol_table[i].symbol ) == 0 ;
                if( cmd_found )
                { 
                    ret = symbol_table[i].command( argv[0], argv[1], argv[2], argv[3] ) ;
                }
            }
        }
        
        return ret ;
    }
}

For very large symbol tables you might want a more sophisticated look-up, but depending on the required performance and determinism, the simple exhaustive search will be sufficient - far faster than the time taken to send the serial data.

Whilst the resource requirement for the symbol table is somewhat higher that the indexed look-up, it is nonetheless ROM-able and will can be be located in Flash memory which on most MCUs is a less scarce resource than SRAM. Being static const the linker/compiler will most likely place the tables in ROM without any specific directive - though you should check the link map or the toolchain documentation n this.

In both cases I have defined the command functions and executer as returning an int. That is optional of course, but you might use that for returning responses to the PC issuing the serial command.

Clifford
  • 88,407
  • 13
  • 85
  • 165
  • What happens with the stack after you've called one of the functions accepting fewer than 4 `int`s? It looks like UB to me and if I add the compiler flags `-Wcast-function-type -g -fsanitize=address,undefined` I get [this](https://godbolt.org/z/Wc9dacY6d) – Ted Lyngmo Mar 22 '22 at 22:05
  • @TedLyngmo : that is determined by the type of the function pointer `tCmdFn` not by the concrete function called, redundant arguments are passed and ignored, the SP will be restored. Of course if you enable warnings to detect 'suspicious' code, you will get warnings, it is probably a bit "dirty", but I have used similar code to implement VxWorks-like command shell on several platforms and toolchains. I am not sure if it is defined, but there are only so many possibilities for compiler construction and call behaviour, and the actual behaviour seems empirically predictable. Take it or leave it – Clifford Mar 22 '22 at 22:46
  • 1
    @TedLyngmo : At https://en.cppreference.com/w/cpp/language/reinterpret_cast point 7 "_...Calling the function through a pointer to a different function type is undefined..._". So yes, it is not defined behaviour. As I said, empirically I have never had it not work as required or to have adverse effect on the stack, at any optimisation level, on any compiler YMMV. It is remarkably simple, efficient on resource constrained systems, and extensible Use advisedly, test carefully. – Clifford Mar 22 '22 at 22:58
  • 1
    @TedLyngmo : One problem that I can see is that is a compiler stacked arguments in a different order; a two argument function would receive arg3 and arg4, so you are right to be concerned. – Clifford Mar 22 '22 at 23:13
  • On ARM (https://godbolt.org/z/jP9MrYGKG) the four argument are passed in R0-R3 and the return value is in R0 - that is defined by the ARM ABI. More than four arguments uses the stack. I have done this for functions with up to 24 arguments. The calling convention is at least (in this case) defined for the architecture if not by the language. Which is why it works and why I would not be too concerned. I would entirely understand if it makes you itchy. – Clifford Mar 22 '22 at 23:21
  • Good to know an nice checking up practical facts! I did some experimenting with passing a more "complex" type than an `int` by value and noticed that I got different results depending on which compiler and which optimization level I used, so yes, it makes me a bit itchy :-) ... but if one knows the target platform and can show that it'll in fact always _do the right thing_, no problem! – Ted Lyngmo Mar 22 '22 at 23:28
  • @TedLyngmo: yes, I have only done this on 32 bit targets, and used the int args to pass pointers. "safer" to use `intptr_t` perhaps. I have implemented a safer method that has matching function pointer types, but it is complex to maintain, more code, and the symbol table is RAM resident and no longer ROMable. – Clifford Mar 23 '22 at 07:43
  • Sounds really cool though. Pehaps with some preprocessing it could be made ROMable? I'm just speculating. I haven't worked much with that small devices with it matters :) – Ted Lyngmo Mar 23 '22 at 18:12