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



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

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

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

对于我的c++handleCommands函数来说,有没有更好的方法可以选择在不进行大量大小写切换或if-else语句的情况下运行可能的100个函数中的哪一个?

代码摘录:

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>ect和一些需要参数的函数;眨眼,10>lt;runto,90>lt;设置模式,锁定>

在python中对照csv文件进行验证,并且发送到微控制器的实际串行消息被发送为<(csvfile中comand的索引),参数>其中<gt;和,作为分隔符。

因此,用户将键入blink,10,python应用程序将发送<1,10>由于在csv文件的索引1中发现了blink,因此过度串行。

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

由于我在微控制器上运行这个程序,我真的不想在flash中存储一个可能的命令的长文件,因此在python gui端进行验证。

注意,在可能的多参数函数的情况下,比如<移动,90,30>即在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是一个解释器,如果命令行解析并执行正常,它将返回true。在上面的示例中,命令1取一个参数,2取两个参数,3取一个(string),而4没有参数。

命令映射到编号的映射可能是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,
};

CCD_ 9将对命令行进行最基本的验证(检查<>),并将其放入CCD_

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

这是一个现场演示。

为每个";命令";可以使用简单的函数指针查找表。例如:

#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应用程序符号表和嵌入式查找表同步。在嵌入的目标上具有符号表可能不是禁止性的;例如:

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

对于非常大的符号表,您可能需要更复杂的查找,但根据所需的性能和确定性,简单的彻底搜索就足够了,远远快于发送串行数据所需的时间。

虽然符号表的资源需求比索引查找略高,但它仍然是可ROM的,并且可以位于闪存中,在大多数MCU上,闪存是比SRAM更少稀缺的资源。作为static const,链接器/编译器很可能会在没有任何特定指令的情况下将表放在ROM中——尽管你应该检查链接映射或工具链文档。

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

您所说的是远程过程调用。因此,您需要有一些机制来序列化和反序列化调用。

如注释中所述,您可以制作一个从cmd到实现该命令的函数的映射。或者只是一个数组。但问题仍然存在,不同的函数需要不同的参数。

因此,我的建议是使用vardiac模板添加一个包装器函数。

在每个命令前面加上命令的数据长度,这样接收器就可以读取命令的数据块,并知道何时将其调度到函数。然后,包装器获取数据块,将其拆分为每个参数的适当大小,并对其进行转换,然后调用读取函数。

现在,您可以制作这些包装器函数的映射或数组,每个函数都绑定到一个命令,编译器将根据这些类型为您生成反序列化代码。(对于每种类型,您仍然需要做一次,编译器只为完整的函数调用组合这些类型)。

最新更新