不知道下面的代码高亮了没有,我加了 ```c++,但是预览下的没高亮。应该是支持高亮的,我看其他的帖子里面有高亮的代码。
新做的一个项目,本来是用 Rust 写的,包管理很方便。但是组长说 Rust 招不到人,就让我用 C++ 重写。C++ 的话现在用的是 git submodules 和 cmake ,没那么爽。好在组长没说 C++ 的版本,应该能用上 C++20 。
p>本来是面向对象的命令式写法,但是网上冲浪太多了,经常看到函数式,早就想试试了,但其实自己面向对象也才懂一点点。最近想把测试写起来,因为自己的代码的其中两个 bug 让项目延期了几天,如果能写测试的话项目就不会在我这里卡住了。如果是面向对象的测试的话,我看一般是定义一个接口,然后实现里面的纯虚函数,然后写 mock 类测试。但这样的话就需要为了测试去额外定义一个接口。我就想试试函数式,虽然我还不确定函数式能否让测试变得简单、没有额外负担。
下面的代码和测试无关,我只是想体验一下函数式。我想的是把那一大堆的 if 用 std::map 来代替,同时这个 std::map 在添加子命令的时候自动生成,这样子 main 函数的可读性就比原来好多了,行数也比较少。还有就是上面定义了变量,下面又传递给 SubCommand ,感觉复用度太低了,也许可以用一个结构体,然后和 SubCommand 组合一下。但是不知道具体怎么写。
有偿的话不知道 200 元行不行。
int main(int argc, char **argv) { // parse command line CLI::App cli; string logLevel = "info"; cli.add_option("-l,--logLevel", logLevel, "log level") ->option_text("debug, info, warn, error") ->capture_default_str(); string logFile = "stdout"; cli.add_option("-f,--logFile", logFile, "log file")->capture_default_str(); bool version = false; cli.add_flag("-v,--version", version, "show version")->capture_default_str(); auto &serveCli = *cli.add_subcommand("serve", "serve uart-manager service"); auto &readDataCli = *cli.add_subcommand("readData", "read data from uart-manager service"); uint32_t readDataPort; readDataCli.add_option("-p,--port", readDataPort, "uart port")->required(); uint32_t messages = 1; readDataCli.add_option("-m,--messages", messages, "stop after read ? messages") ->capture_default_str(); auto &setBaudRateCli = *cli.add_subcommand("setBaudRate", "set uart baud rate"); uint32_t setBaudRatePort; setBaudRateCli.add_option("-p,--port", setBaudRatePort, "uart port")->required(); uint32_t setBaudRateBaudRate; setBaudRateCli.add_option("-b,--baudRate", setBaudRateBaudRate, "uart baud rate") ->option_text("9600, 14400, 19200, 38400, 57600, 115200") ->required(); auto &writeDataCli = *cli.add_subcommand("writeData", "write data to uart-manager service"); vector<uint32_t> writeDataPorts; writeDataCli.add_option("-p,--port", writeDataPorts, "uart port")->required(); string writeDataHex; writeDataCli.add_option("-H,--hex", writeDataHex, "hex data"); string writeDataStr; writeDataCli.add_option("-s,--str", writeDataStr, "string data"); auto &updateFpgaCli = *cli.add_subcommand("updateFpga", "update fpga"); string updateFpgaFile; updateFpgaCli.add_option("-f,--file", updateFpgaFile, "fpga file")->required(); auto &getAutoRespOnseCli= *cli.add_subcommand("getAutoResponse", "get auto response"); uint32_t getAutoResponsePort; getAutoResponseCli.add_option("-p,--port", getAutoResponsePort, "uart port")->required(); auto &clearAutoRespOnseCli= *cli.add_subcommand("clearAutoResponse", "clear auto response"); uint32_t clearAutoResponsePort; clearAutoResponseCli.add_option("-p,--port", clearAutoResponsePort, "uart port")->required(); auto &addAutoRespOnseCli= *cli.add_subcommand("addAutoResponse", "add auto response"); uint32_t addAutoResponsePort; addAutoResponseCli.add_option("-p,--port", addAutoResponsePort, "uart port")->required(); vector<string> addAutoResponseHex; addAutoResponseCli.add_option("-H,--hex", addAutoResponseHex, "response hex data"); vector<string> addAutoResponseStr; addAutoResponseCli.add_option("-s,--str", addAutoResponseStr, "response string data"); vector<string> addAutoResponseWhenContains; addAutoResponseCli .add_option("-w,--whenContains", addAutoResponseWhenContains, "response str or hex data when contains str or hex data") ->required(); auto &getFpgaVersiOnCli= *cli.add_subcommand("getFpgaVersion", "get fpga version"); auto &set12vStatusCli = *cli.add_subcommand("set12vStatus", "set 12v of ports 1-48 status"); auto set12vStatusEnableFirst = true; set12vStatusCli .add_flag("-e,--enableFirst", set12vStatusEnableFirst, "enable first 12v, ports 1-24") ->capture_default_str(); auto set12vStatusEnableSecOnd= true; set12vStatusCli .add_flag("-s,--enableSecond", set12vStatusEnableSecond, "enable second 12v, ports 25-48") ->capture_default_str(); auto &get12vStatusCli = *cli.add_subcommand("get12vStatus", "get 12v of ports 1-48 status"); CLI11_PARSE(cli, argc, argv); // version if (version) { printf("version: %s\n", VERSION); return 0; } // log initLog(logLevel, logFile); // subcommands if (serveCli) { Manager().start(); } else if (readDataCli) { SubCommand().readData(readDataPort, messages); } else if (setBaudRateCli) { SubCommand().setBaudRate(setBaudRatePort, setBaudRateBaudRate); } else if (writeDataCli) { SubCommand().writeData(writeDataPorts, writeDataHex, writeDataStr); } else if (updateFpgaCli) { SubCommand().updateFpga(updateFpgaFile); } else if (getAutoResponseCli) { SubCommand().getAutoResponse(getAutoResponsePort); } else if (clearAutoResponseCli) { SubCommand().clearAutoResponse(clearAutoResponsePort); } else if (addAutoResponseCli) { SubCommand().addAutoResponse(addAutoResponsePort, addAutoResponseHex, addAutoResponseStr, addAutoResponseWhenContains); } else if (getFpgaVersionCli) { SubCommand().getFpgaVersion(); } else if (set12vStatusCli) { SubCommand().set12vStatus(set12vStatusEnableFirst, set12vStatusEnableSecond); } else if (get12vStatusCli) { SubCommand().get12vStatus(); } else { cout << cli.help() << endl; } return 0; }
![]() | 1 buf1024 2023-10-30 13:15:02 +08:00 伪代码示意 ``` std::list xx; class Cmd { bool* isSet; func handler }; cmd 定义以下宏定义 #define AddCmd do { auto &get12vStatusCli = *cli.add_subcommand("get12vStatus", "get 12v of ports 1-48 status"); xx.push(new Cmd{get12vStatusCli, handler...}) } while (0) 最后的 if for cmd in xx if (cmd.isSet) { xx() } ``` |
![]() | 2 s7964926 2023-10-30 13:43:31 +08:00 ```cpp #include <map> #include <functional> #include <CLI/CLI.hpp> struct CmdOptions { bool serveCli = false; uint32_t readDataPort = 0; uint32_t messages = 1; }; class SubCommand { public: void serve(const CmdOptions &options) { // 实现 } void readData(const CmdOptions &options) { // 使用 options.readDataPort 和 options.messages } // 其他 }; int main(int argc, char **argv) { CLI::App cli; CmdOptions options; SubCommand subCmd; auto &serveCli = *cli.add_subcommand("serve", "serve uart-manager service"); serveCli.callback([&options, &subCmd]() { subCmd.serve(options); }); auto &readDataCli = *cli.add_subcommand("readData", "read data from uart-manager service"); readDataCli.add_option("-p,--port", options.readDataPort, "uart port")->required(); readDataCli.add_option("-m,--messages", options.messages, "stop after read ? messages"); readDataCli.callback([&options, &subCmd]() { subCmd.readData(options); }); // 其他 CLI11_PARSE(cli, argc, argv); return 0; } ``` |
5 FH0 OP ```c++ compose( newCli, serve, readData, setBaudRate, ); ``` ```c++ cli.map(serve) .map(readData) .map(setBaudRate); ``` |
![]() | 7 luassuns 2023-10-30 15:23:27 +08:00 ```cpp struct Command { std::string msg; std::string description; bool require; std::function<void(std::any)> command; void exec(std::any v) { if (command) command(v); } }; class Args { public: Args *add_opt(Command *command) { commandMap[command->msg] = command; return this; } void parse(int argc, char *argv[]) { // do parse.... std::map<std::string, std::any> valueMap; for (auto &[key, value] : commandMap) { if(valueMap.find(key) != valueMap.end()) { value->exec(valueMap[key]); } else { if(value->require) { // require but not found } } } } std::map<std::string, Command *> commandMap; }; Args *args = new Args(); args->add_opt( new Command{"-p", "port", true, [](std::any v) { std::cout << std::any_cast<int>(v); }}) ->add_opt(new Command()) ->add_opt(new Command()) ->add_opt(new Command()) ->add_opt(new Command()); ``` |
8 FH0 OP @luassuns return this 确实可以,但是方法都是封装在一个类里面的,这是面向对象的思想。我想尝试一下函数式,就是纯函数、副作用、柯里化、函子那些。 |
![]() | 9 netabare 2023-10-30 16:44:20 +08:00 via Android ![]() c++没必要太函数式吧,如果 op 想要用的话可以试试 optional 来替代返回错误码/exception 的写法,但不确定 20 是否支持流式的 flatmap 写法。 感觉用好 move 和 unique_ptr 就好了,c++真的没必要太追求纯函数或者柯里化。 |
11 kilasuelika 2023-10-30 23:18:27 +08:00 via Android boost 里面有个 hof ,高阶函数,可以去了解一下。c++ 本身不是专门做函数式的,要想实现那些必须要进行一些封装和借助一些奇技淫巧。 |
12 FH0 OP @kilasuelika OK |
![]() | 13 netabare 2023-10-31 08:28:35 +08:00 via Android ![]() @FH0 其实如果要做函数式的话,C#、Java 之类的虚拟机托管语言会合适很多,至少写起来没那么痛苦。 函数式写法主要还是追求更好的抽象和表达能力,比较吃编译器的代码优化和语法糖。在 C++这种偏底层,程序员希望能够对内存和变量生命周期有更多掌控权的语言里,有点吃力不讨好。Rust 里面其实也没多少函数式的写法,更多是从函数式编程里面借用了比如模式匹配、trait 之类的思路。 不过 C++也没有模式匹配或者 trait ,所以还是有点尴尬。 |
14 FH0 OP @netabare 我找到了一个项目用了很多的函数式思想,还是想探索一下。github.com/arximboldi/ewig |
15 FH0 OP 最后代码优化成了这样,先暂时用着。 ```c++ int main(int argc, char **argv) { auto [mainCli, beforeSubcommand] = makeCli(argc); const auto subcommands = { serve(), readData(), writeData(), setBaudRate(), clearAutoResponse(), getAutoResponse(), addAutoResponse(), clearTimedResponse(), getTimedResponse(), addTimedResponse(), get12vStatus(), set12vStatus(), getFpgaVersion(), updateFpga(), }; unordered_map<App *, function<void()>> subcommandMap; for (const auto &[cli, callback] : subcommands) { mainCli->add_subcommand(cli); subcommandMap.emplace(&*cli, callback); } CLI11_PARSE(*mainCli, argc, argv); beforeSubcommand(); for (auto *key : mainCli->get_subcommands()) { subcommandMap[key](); } return 0; } ``` |