……在全世界范围内,这是最受推崇、设计最精巧的 C++ 库项目之一。
— Herb Sutter 和 Andrei Alexandrescu, C++ Coding Standards
本节重点介绍一些设计问题。
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 输入和 Unicode 输入将使用 codecvt 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 稍慢。
空间:UTF-8 在输入为 ascii 时占用空间更少。
代码大小:UTF-8 需要额外的转换代码。然而,它允许使用现有的解析器,而无需将其转换为 std::wstring
,并且此类转换很可能会创建许多新的实例化。
没有明确的领先者,但最后一点似乎很重要,因此将使用 UTF-8。
选择 UTF-8 编码允许使用现有的解析器,因为 7 位 ascii 字符在 UTF-8 中保留其值,因此搜索 7 位字符串很简单。但是,有两个微妙的问题:
我们需要假设字符字面量使用 ascii 编码,而输入使用 Unicode 编码。
一个 Unicode 字符(例如 '=')后面可能跟着一个“组合字符”,组合后的结果与单独的 '=' 不同,因此简单地搜索 '=' 可能会找到错误的字符。
实际上,这两个问题似乎都不算关键,因为 ascii 是几乎通用的编码,而且 '='(以及对库有特殊含义的其他字符)后面不太可能出现组合字符。