本节重点介绍一些设计问题。
在正式评审期间,“Unicode 支持”是一项被特别要求的特性。在本文档中,“Unicode 支持”是“wchar_t”支持的同义词,假定“wchar_t”始终使用 Unicode 编码。另外,当我们提到“ascii”(小写)时,我们不是指严格的 7 位 ASCII 编码,而是指本地 8 位编码的“char”字符串。
通常,“Unicode 支持”可能意味着很多事情,但对于 program_options 库而言,它意味着:
每个解析器都应该能够接受 char* 或 wchar_t*,并能正确地将输入分割成选项名和选项值,然后返回数据。
对于每个选项,应该能够指定从字符串到值的转换是使用 ascii 还是 Unicode。
该库保证:
ascii 输入将原样传递给 ascii 值。
Unicode 输入将原样传递给 Unicode 值。
传递给 Unicode 值(来自 ascii 输入)和传递给 ascii 值(来自 Unicode 输入)的情况将使用 codecvt facet 进行转换(该 facet 可以由用户指定)。
关键在于,可以将一些“ascii 选项”与“Unicode 选项”一起使用。原因有两个。首先,对于某个特定类型,您可能没有从 Unicode 字符串中提取值的代码,而且不应该强制编写这样的代码。其次,设想一个可重用的库,它有一些选项并通过其接口公开选项描述。如果所有选项都是 ascii 或 Unicode,而库本身不使用任何 Unicode 字符串,那么库的作者很可能会使用 ascii 选项,从而导致该库在 Unicode 应用程序中不可用。本质上,这将需要提供库的两个版本——ascii 版和 Unicode 版。
另一个重要的点是 ascii 字符串会原样传递,不会修改。换句话说,无法简单地将 ascii 转换为 Unicode 然后进一步处理 Unicode。问题在于默认的转换机制——codecvt facet——在没有额外设置的情况下可能无法处理 8 位输入。
上述 Unicode 支持并非完整。例如,我们不支持 Unicode 选项名。Unicode 支持非常复杂,需要 Boost 整个库的解决方案。即使是比较两个任意的 Unicode 字符串也并非易事。最后,在选项名中使用 Unicode 与国际化有关,这本身就具有复杂性。例如,如果选项名取决于当前区域设置,那么所有程序部分以及使用该名称的其他部分都必须进行国际化。
实现 Unicode 支持的首要问题是使用模板和 std::basic_string,还是使用某种内部编码并在接口边界进行内部编码和外部编码之间的转换。
选择主要在于代码大小和执行速度。基于模板的解决方案要么将库代码链接到使用该库的每个应用程序中(从而不可能使用共享库),要么在共享库中提供显式实例化(增加其大小)。基于内部编码的解决方案不可避免地会在多个地方进行转换,并且速度会稍慢一些。鉴于速度通常不是该库的问题,第二种解决方案看起来更具吸引力,但我们会仔细考察各个组件。
对于解析器组件,我们有三种选择:
使用完全基于模板的实现:给定某种类型的字符串,解析器将返回一个包含相同类型字符串的 parsed_options 实例(即 parsed_options 类将是模板化的)。
使用内部编码:与上面相同,但字符串将转换为内部编码的格式,或从内部编码的格式转换过来。
使用并部分暴露内部编码:与上面相同,但 parsed_options 实例中的字符串将使用内部编码。这可能会避免在 parsed_options 实例直接传递给其他组件时进行额外的转换,但这也可能对用户来说是危险的或令人困惑的。
第二种解决方案似乎是最好的——它不会显著增加代码大小,并且比第三种更清晰。为了避免额外的转换,parsed_options 的 Unicode 版本也可以以内部编码存储字符串。
对于选项描述组件,我们没有太多选择。由于不希望所有选项都使用 ascii 或所有选项都使用 Unicode,而是希望同时支持一些 ascii 和一些 Unicode 选项,因此 value_semantic 的接口必须能够同时处理两者。唯一的方法是传递一个额外的标志,指示字符串使用的是 ascii 还是内部编码。然后,value_semantic 的实例可以根据需要转换为其他编码。
对于存储组件,唯一受影响的函数是 store。对于 Unicode 输入,store 函数应该将值转换为内部编码。它还应该通知 value_semantic 类正在使用的编码。
最后,我们应该使用什么内部编码?备选方案是:std::wstring(使用 UCS-4 编码)和 std::string(使用 UTF-8 编码)。替代方案之间的区别在于:
速度:UTF-8 稍慢。
空间:当输入是 ascii 时,UTF-8 占用的空间更少。
代码大小:UTF-8 需要额外的转换代码。但是,它允许使用现有的解析器而不将其转换为 std::wstring,而且此类转换很可能会创建许多新的实例化。
没有明确的赢家,但最后一点似乎很重要,所以将使用 UTF-8。
选择 UTF-8 编码允许使用现有的解析器,因为 7 位 ascii 字符在 UTF-8 中保留其值,所以搜索 7 位字符串很简单。然而,有两个微妙的问题:
我们需要假设字符字面量使用 ascii 编码,而输入使用 Unicode 编码。
Unicode 字符(例如 '=')后面可能跟着一个“组合字符”,其组合方式与单独的 '=' 不同,因此简单搜索 '=' 可能会找到错误的字符。
这两个问题在实践中似乎都不算关键,因为 ascii 是几乎通用的编码,而且 '='(以及对库有特殊含义的其他字符)后面不太可能出现组合字符。