C++,从用户调用许多可能的函数的有效方法

C++, efficient way to call many possible functions from user

我对 C++ 比较陌生,主要使用 python。

我有一个场景,其中用户(我)使用 GUI 通过串行向微控制器发送命令,然后微控制器处理它们。

现在我有 10 个命令,但随着项目的发展(某种形式的模块化机器人),我可以设想有 50-100 个可能的命令。

我的 c++ handleCommands 函数是否有更好的方法 select 可能的 100 个函数中的哪一个 运行 而无需进行大量大小写切换或 if else 语句?

代码摘录:

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;

这对于 3-4 个函数来说效果很好,但在我看来这并不是实现更多函数的最佳方式。

我听说过查找表,但由于每个函数都不同,可能需要或不需要参数,所以我一直在思考如何实现它们。

设置的一些背景:

这些命令主要是诊断命令,< ID > 等和一些需要参数的功能命令,例如 <运行to,90>

验证是在 python 中针对 csv 文件完成的,发送到微控制器的实际串行消息作为 <(csvfile 中命令的索引),参数> 发送,其中 < > 和 , 是分隔符。

因此用户将键入 blink,10,并且 python 应用程序将通过串行发送 <1,10>,因为在 csv 文件的索引 1 处找到了 blink。

微控制器读入这些,我剩下 2 个字符数组,命令数组包含一个数字,值数组包含发送的值。(也是一个数字)

因为我运行在微控制器上使用它,所以我真的不想在闪存中存储可能的命令的长文件,因此在 python gui 上完成了验证边.

请注意,在可能的多参数函数的情况下,比如 即在 30 秒内移动 90 度,实际函数只会接收一个参数“30,90”,然后拆分根据需要添加。

如果您的命令以

格式通过串行线路传输
<command-mapped-to-a-number,...comma-separated-parameters...>

我们可以这样模拟:

#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 上面的解释器将 return true 如果命令行被解析并执行正常。在上面的示例中,命令 1 接受一个参数,2 接受两个参数,3 接受一个 (string) 而 4 没有参数。

来自 command-mapped-to-a-number 的映射可能是 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,
};

exec 会对命令行进行最基本的验证(检查 <>)并将其放在 std::istringstream 中以便于提取此命令行的信息:

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
}

剩下唯一棘手的部分是命令和仿函数的 unordered_map。 这是一个开始:

// 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;
     }},
};

如果将它们放在一起,您将从成功解析(和执行)收到的所有命令行中获得以下输出:

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

这是一个 live demo

你说的是远程过程调用。所以你需要有一些机制来序列化和 un-serialize 调用。

如评论中所述,您可以制作从 cmd 到执行命令的函数的映射。或者只是一个数组。但问题仍然是不同的函数需要不同的参数。

所以我的建议是使用 vardiac 模板添加包装函数。

在每个命令前加上命令的数据长度,以便接收方可以读取命令的数据块并知道何时将其分派给函数。然后包装器获取数据块,将其拆分为每个参数的正确大小并进行转换,然后调用读取函数。

现在您可以制作这些包装函数的映射或数组,每个都绑定到一个命令,编译器将从类型中为您生成 un-serialize 代码。 (您仍然需要为每种类型执行一次,编译器只会将它们组合起来用于完整的函数调用)。

给定每个“命令”的整数索引,可以使用一个简单的函数指针 look-up table。例如:

#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 ;
    }
}

命令执行传递四个参数(您可以根据需要扩展它)但是对于采用较少参数的命令函数,它们将只是将被忽略的“虚拟”参数。

您提议的索引转换有些容易出错且维护繁重,因为它需要您保持 PC 应用程序符号 table 和嵌入式查找 table 同步。在嵌入式目标上使用符号 table 可能不是禁止的;例如:

#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 ;
    }
}

对于非常大的符号 tables,您可能需要更复杂的 look-up,但根据所需的性能和确定性,简单的穷举搜索就足够了 - 比所花费的时间快得多发送串行数据。

虽然符号 table 的资源要求略高于索引 look-up,但它仍然是 ROM-able 并且可以位于大多数 MCU 上的闪存中是一种比 SRAM 更不稀缺的资源。作为 static const,linker/compiler 很可能会在没有任何特定指令的情况下将 table 放入 ROM - 尽管您应该检查 link 映射或工具链文档。

在这两种情况下,我都将命令函数和执行器定义为返回 int。这当然是可选的,但您可以使用它来返回对发出串行命令的 PC 的响应。