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 > 等和一些需要参数的功能命令,例如
验证是在 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 的响应。
我对 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 > 等和一些需要参数的功能命令,例如
验证是在 python 中针对 csv 文件完成的,发送到微控制器的实际串行消息作为 <(csvfile 中命令的索引),参数> 发送,其中 < > 和 , 是分隔符。
因此用户将键入 blink,10,并且 python 应用程序将通过串行发送 <1,10>,因为在 csv 文件的索引 1 处找到了 blink。
微控制器读入这些,我剩下 2 个字符数组,命令数组包含一个数字,值数组包含发送的值。(也是一个数字)
因为我运行在微控制器上使用它,所以我真的不想在闪存中存储可能的命令的长文件,因此在 python gui 上完成了验证边.
请注意,在可能的多参数函数的情况下,比如
如果您的命令以
格式通过串行线路传输<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 的响应。