tuple

I heard you like tuple so i put tuple in your tuple so you have tuple in your tuple - Yo Dawg (Source)

由于微信会对外部页面重新排版,在微信内置浏览器中,本文会出现下面的异常:

  • 文中的链接无法正常访问
  • 文中的代码段落多出了许多 html tag,变得不可读

请选择右上角菜单“在浏览器中打开”即可正常阅读。(本文的永久链接)

需求

今天上午写代码时,写着写着遇到一个需求,是在 C++ 程序里把一个命令行字符串解析成对应的一组变量。也就是把形如:

app.exe foo 100 bar 0.05

这样的一个字符串解析一下,放到下面这样的一组变量里:

struct VariableGroup
{
    std::string   name = "";
    int           id = 0;
    std::string   description = "";
    float         precision = 0.0f;
};

这个需求很常见,相信大家都遇到过吧。在实际的程序里,同一个命令行可以有各种不同的用法,命令和参数,全部都手写的话,很罗嗦,维护也麻烦。如果是 Python,有现成的 argparsedocopt,C++ 的话,选择就少一些了,而我又不想用 boost::tokenizer 之类的库,脑子里突然闪过 std::tuple 这货。好吧,就是它了,试着手写一个类型安全,简洁轻便,又能有一定灵活性的版本吧。


我们知道,std::tuple (以下简洁起见直接说 tuple) 最好玩的特性就是__可以装 n 个任意类型的变量__(一个加强版的 std::pair)正好对应“__命令行字符串的参数类型和个数的随意组合__”的需求。

老规矩,先定义接口:

std::tuple<...> parse(const std::string& commandline)
{
    ...
    return ???;
}

上面的代码中,接收一个字符串,返回一个 tuple 包含解析好的各项数据。如果能定义出这样的接口,那么用的时候我们就可以这样了:

auto v = parse<std::string, int, float>("foo 123 0.05");

这样解析过之后 v 就是一个包含了三个元素的 tuple,分别是 std::string "foo"int 123float 0.05。这个 parse() 函数应该可以接收任意数目任意类型的命令行参数,返回一个内含对应数目和类型的 tuple

清楚,简洁和方便,对吧 ^_^


目标一明确,就可以开始挽起袖子写代码了。

......

约过了一炷香时间,(好吧,两柱香 -_-)写好了。现在回头对一下前面提出的接口,能满足我们“既类型安全,又简洁轻便,还有一定灵活性”的需求吗?我们一起来看看吧。


接口

实际写好的功能以类 BtArgParser 的形式提供:

class BtArgParser
{
public:
    BtArgParser(const std::string& args);
    ...
    template<class... Args> std::tuple<Args...> parse_tuple() { ... }
    template<class... Args> std::tuple<Args...> parse_tuple_tolerated() { ... }
    template<class... Args> bool parse_tuple_no_throw(std::tuple<Args...>* out_tuple) { ... }
};

这个类的构造函数接收一个命令行字符串,然后用户可以选择使用三个成员函数之一,完成解析的任务。

这时候你要问了,不是说好的一个接口吗,怎么多出了两个来?

别急,且容俺一一道来。

我们知道,根据经验,传入的字符串一般是用户在终端敲进去的。既然是用户输入,就有出错的可能,可能敲少了,敲多了,命令敲错了,类型弄错了,等等等等……咱们的 parse() 函数需要在遇到情况的时候,告诉用户出错了。瞅瞅前面的用例,返回值已经被 tuple 占据了,难道让我们学 map::insert 那样,返回一个 pair<iterator, bool> 吗?(喂喂喂,说好的简洁轻便呢?)

这里通过提供三个命名迥异的函数,提供不同的处理策略。

  1. __parse_tuple()__ 返回 tuple ,遇到错误时抛对应的异常(同标准库大多数函数的行为一致)
  2. __parse_tuple_tolerated()__ 返回 tuple,遇到错误尽可能恢复,并尽可能返回有效数据,不报错
  3. __parse_tuple_no_throw()__ 不抛异常,但返回 bool 表示是否解析成功,tuple 以参数返回

这个看起来啰嗦的方案,实际上是让代码自带注释属性,而且顺便做到对查找友好 (grep-friendly) 的。来吧,跟我一起念,代码可读性是程序员生活质量的保证~~


parse_tuple() 标准版

第一个函数 parse_tuple() 没啥好说的,使用姿势就和前面的接口描述几乎一致:

BtArgParser p(args);
auto v = p.parse_tuple<std::string, int, float>();

注意这是抛异常的版本,如果不手动 try / catch 处理错误的话,当发生错误时会一层一层 unwind 直到程序退出或被外层捕获。这里可能抛出的异常都在 std::exception 之下。


parse_tuple_tolerated() 容错版

从上面可以知道,第二个函数 parse_tuple_tolerated() 会尽可能地把传进来的字符串和需求方 tuple 内的变量类型匹配,那么具体的策略是什么呢?如果命令行提供了多余的参数,那么直接忽略;如果参数不够,那么就补充空的字符串参数;如果类型不匹配,那么使用 0 (对应 int 和 float) 和 "" (对应 std::string) 去构造默认值。总之,能恢复就恢复,能分析出多少分析多少。

如下所示,当运行这一句代码时:

auto v = p.parse_tuple_tolerated<std::string, int, float>();

有意地提供下图中黑体字部分以 ed scale 开头的两条(不匹配的)命令

  1. ed scale __foo 1.32 bar__
  2. ed scale __foo bar__

(注意,黑体字部分将被解析为三个参数依次为 'string', 'int' 和 'float' 的 tuple):

info_2

紧接着命令的一行输出是返回的解析结果。简单描述一下,

第一次匹配 (匹配内容为 "foo 1.32 bar") 时,

  • "foo" 解析为 std::string 分析成功
  • "1.32" 解析为 int 强转为 1 分析成功
  • "bar" 解析为 float 失败,得到使用 0 构造的 0.0

结果是能转换的就转换,转不了的返回 0。

第二次匹配 (匹配内容为 "foo bar") 时,

  • "foo" 解析为 std::string 分析成功
  • "bar" 解析为 int 失败,得到 0
  • 第三个参数没提供,直接返回 0.0

结果是参数数目不符时,用零补足。

可以看到,使用 parse_tuple_tolerated() 来做高容忍度的匹配,总会返回有效的 tuple (当然里面的值并不总是有意义)。有 Lua 经验的程序员可能已经看出来了,这与 Lua 处理函数参数时的容错机制 (不够就补足,多了就忽略) 是类似的。


parse_tuple_no_throw() 静音版

呼,喝口水,接着说第三个函数 parse_tuple_no_throw() 。这个函数除了把抛异常改为返回 bool (以及由此导致的使用参数来返回 tuple) 以外,内部行为与第一条保持一致。需要补充说明的是,仅靠返回 bool 来表示是否成功,一些情况下是不够的。实践中我把错误细节打印到了日志中,当需要调试的时候可以查看日志,如下所示:

运行代码:

std::tuple<std::string, int, float> v;
if (!p.parse_tuple_no_throw(& v))
    return;

然后有意地提供下面图中黑体字部分的命令行参数,就可以看到如下图中的报错信息了

info_1

可以看到,

  • 第一次,我们传了 "foo""bar" 两个字符串进来,报了错误 std::logic_error:参数个数不匹配,需要三个,传了两个。
  • 第二次我们传了 123"foo""bar" 三个参数进来,又报 std::bad_cast 的错了:有类型转换失败,具体细节是第 1 个参数从 std::stringint 转换失败的。注意,这里的第一个实际上是第二个,我们保持 C/C++ 的传统,从 0 开始计数。

在这一例中,123 被成功地解析成字符串 "123" 而解析第二个参数 foo 时遇到了错误,无法转成 int,就报错并返回 false 退出了。


细节

好了,介绍到这里,本文就应该结束了。下次如果时间充裕的话,我们再来聊一下 BtArgParser 内部的实现吧。有兴趣自己看的同学,代码的传送门在这里 (BtArgParser.h) 这里 (BtArgParser.cpp) 。预备知识是 std::tuplestd::tuple_sizestd::tuple_element<...>::type,以及可变参模板模板偏特化模板递归的语意,这几点如果有疑问的话,可以点对应的链接进去了解一下。


不多说了,要回去继续撸代码了。不得不说,写代码比写文章好玩多了~~

Gu Lu
2015-12-22


[注]

Comments
Write a Comment

Tags

游戏开发   随笔   Programming   C/C++   优化   Unity   C++   知乎   游戏设计   比特币   Unity3D   区块链   软件开发   引擎设计   系统架构   Production   idtech   中国文化   加密货币   项目管理   Bitcoin   游戏评论   资源管理   资源流水线   效率   道德经   网络   方法论   模板编程   Lua   Oculus   GDC   渲染   VR   PerfAssist   Unity MemoryProfiler   BCH   经济学   信息过载   行业报告   字体   Productivity   图形   网络编程   Dice   协程   EMC   Premake   测试   中间件   Game Engine   新手引导   Methodology   CI   命令行解析   goroutine   ndk   Ethereum   nanomsg   自动化   Scripting   摘录   Debugging   同步技术   cppcon   C++模板   DOOM3   技术评估   Unity GC   C++11   学习方法   Surface Pro 3   Engine Evaluation   CRT   文化   Blockchain   笔记   golang   图形编程   多线程   ETH   Blockchain Computing   Bitcoin Cash   cppcon14   Bitcoin SV   Visual Studio   Unity Coroutine   跨语言可变参数列表   团队协作   货币   Deployment   Visual Assist   工程改进   Michael Abrash   exp   开放世界   域名   虚拟现实   系统重构   slua   遮挡剔除   完美转发   协作式调度   Modern C++   类型推导   Memory Debugging   个人成长   小故事   BTC   暴雪   产品   历史   错误处理   Unity Profiler   MOD  

知识共享许可协议
本作品由Gu Lu创作,采用知识共享Attribution-NonCommercial-NoDerivatives 4.0 国际许可协议进行许可。