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
这样的一个字符串解析一下,放到下面这样的一组变量里:
|
|
这个需求很常见,相信大家都遇到过吧。在实际的程序里,同一个命令行可以有各种不同的用法,命令和参数,全部都手写的话,很罗嗦,维护也麻烦。如果是 Python,有现成的 argparse 和 docopt,C++ 的话,选择就少一些了,而我又不想用 boost::tokenizer
之类的库,脑子里突然闪过 std::tuple
这货。好吧,就是它了,试着手写一个类型安全,简洁轻便,又能有一定灵活性的版本吧。
我们知道,std::tuple
(以下简洁起见直接说 tuple
) 最好玩的特性就是__可以装 n 个任意类型的变量__(一个加强版的 std::pair
)正好对应“命令行字符串的参数类型和个数的随意组合”的需求。
老规矩,先定义接口:
|
|
上面的代码中,接收一个字符串,返回一个 tuple
包含解析好的各项数据。如果能定义出这样的接口,那么用的时候我们就可以这样了:
|
|
这样解析过之后 v
就是一个包含了三个元素的 tuple
,分别是 std::string "foo"
、int 123
和 float 0.05
。这个 parse()
函数应该可以接收任意数目任意类型的命令行参数,返回一个内含对应数目和类型的 tuple
。
清楚,简洁和方便,对吧 ^_^
目标一明确,就可以开始挽起袖子写代码了。
……
约过了一炷香时间,(好吧,两柱香 -_-)写好了。现在回头对一下前面提出的接口,能满足我们“既类型安全,又简洁轻便,还有一定灵活性”的需求吗?我们一起来看看吧。
接口
实际写好的功能以类 BtArgParser
的形式提供:
|
|
这个类的构造函数接收一个命令行字符串,然后用户可以选择使用三个成员函数之一,完成解析的任务。
这时候你要问了,不是说好的一个接口吗,怎么多出了两个来?
别急,且容俺一一道来。
我们知道,根据经验,传入的字符串一般是用户在终端敲进去的。既然是用户输入,就有出错的可能,可能敲少了,敲多了,命令敲错了,类型弄错了,等等等等……咱们的 parse()
函数需要在遇到情况的时候,告诉用户出错了。瞅瞅前面的用例,返回值已经被 tuple
占据了,难道让我们学 map::insert
那样,返回一个 pair<iterator, bool>
吗?(喂喂喂,说好的简洁轻便呢?)
这里通过提供三个命名迥异的函数,提供不同的处理策略。
- parse_tuple() 返回
tuple
,遇到错误时抛对应的异常(同标准库大多数函数的行为一致) - parse_tuple_tolerated() 返回
tuple
,遇到错误尽可能恢复,并尽可能返回有效数据,不报错 - parse_tuple_no_throw() 不抛异常,但返回
bool
表示是否解析成功,tuple
以参数返回
这个看起来啰嗦的方案,实际上是让代码自带注释属性,而且顺便做到对查找友好 (grep-friendly) 的。来吧,跟我一起念,代码可读性是程序员生活质量的保证~~
parse_tuple() 标准版
第一个函数 parse_tuple()
没啥好说的,使用姿势就和前面的接口描述几乎一致:
|
|
注意这是抛异常的版本,如果不手动 try / catch 处理错误的话,当发生错误时会一层一层 unwind 直到程序退出或被外层捕获。这里可能抛出的异常都在 std::exception
之下。
parse_tuple_tolerated() 容错版
从上面可以知道,第二个函数 parse_tuple_tolerated()
会尽可能地把传进来的字符串和需求方 tuple
内的变量类型匹配,那么具体的策略是什么呢?如果命令行提供了多余的参数,那么直接忽略;如果参数不够,那么就补充空的字符串参数;如果类型不匹配,那么使用 0 (对应 int 和 float) 和 "" (对应 std::string
) 去构造默认值。总之,能恢复就恢复,能分析出多少分析多少。
如下所示,当运行这一句代码时:
|
|
有意地提供下图中黑体字部分以 ed scale 开头的两条(不匹配的)命令
- ed scale foo 1.32 bar
- ed scale foo bar
(注意,黑体字部分将被解析为三个参数依次为 ‘string’, ‘int’ 和 ‘float’ 的 tuple
):
紧接着命令的一行输出是返回的解析结果。简单描述一下,
第一次匹配 (匹配内容为 "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
来表示是否成功,一些情况下是不够的。实践中我把错误细节打印到了日志中,当需要调试的时候可以查看日志,如下所示:
运行代码:
|
|
然后有意地提供下面图中黑体字部分的命令行参数,就可以看到如下图中的报错信息了
可以看到,
- 第一次,我们传了
"foo"
和"bar"
两个字符串进来,报了错误std::logic_error
:参数个数不匹配,需要三个,传了两个。 - 第二次我们传了
123
,"foo"
和"bar"
三个参数进来,又报std::bad_cast
的错了:有类型转换失败,具体细节是第 1 个参数从std::string
到int
转换失败的。注意,这里的第一个实际上是第二个,我们保持 C/C++ 的传统,从 0 开始计数。
在这一例中,123 被成功地解析成字符串 "123"
而解析第二个参数 foo
时遇到了错误,无法转成 int
,就报错并返回 false
退出了。
细节
好了,介绍到这里,本文就应该结束了。下次如果时间充裕的话,我们再来聊一下 BtArgParser
内部的实现吧。有兴趣自己看的同学,代码的传送门在这里 (BtArgParser) 。预备知识是 std::tuple
,std::tuple_size
和 std::tuple_element<...>::type
,以及可变参模板,模板偏特化及模板递归的语意,这几点如果有疑问的话,可以点对应的链接进去了解一下。
不多说了,要回去继续撸代码了。不得不说,写代码比写文章好玩多了~~
[注]
- 本文同时发在我的知乎专栏 (链接)
(全文完)