Boost.Nowide
|
目录
Boost.Nowide 是最初由 Artyom Beilis 实现的库,它使跨平台的 Unicode 感知编程更容易。
该库提供了标准 C 和 C++ 库函数的实现,使其在 Windows 上输入 UTF-8 感知,而无需使用 Wide API。在非 Windows/POSIX 平台上,则使用 StdLib 等效项的别名,因此不会在那里执行任何转换,因为 UTF-8 已经很常用。
因此,您可以在所有平台上使用与标准库对应的相同名称的 Boost.Nowide 函数,并使用窄字符串,它就可以正常工作。
考虑一个简单的应用程序,它将一个大文件分割成块,以便可以通过电子邮件发送。它需要执行一些非常简单的任务
int main(int argc,char **argv)
std::fstream::open(const char*,std::ios::openmode m)
std::remove(const char* file)
std::cout << file_name
不幸的是,如果文件名包含非 ASCII 字符,则不可能在纯 C++ 中实现这个简单的任务。
使用 API 的简单程序可以在内部使用 UTF-8 的系统上工作——绝大多数 Unix 类操作系统:Linux、Mac OS X、Solaris、BSD。但是,在 Microsoft Windows 下,它会在类似 War and Peace - Война и мир - מלחמה ושלום.zip
的文件上失败,因为原生 Windows Unicode 感知 API 是 Wide-API——UTF-16。
这个非常简单的任务很难以跨平台的方式实现。
Boost.Nowide 提供了一组在 Windows 上具有 UTF-8 感知的标准库函数,使 Unicode 感知编程更容易。
该库提供
main
的 argc
、argc
和 env
参数使用 UTF-8 的类cstdio
函数fopen
freopen
remove
rename
cstdlib
函数system
getenv
setenv
unsetenv
putenv
fstream
filebuf
fstream/ofstream/ifstream
iostream
cout
cerr
clog
cin
所有这些函数都可以在 Boost.Nowide 中同名的头文件中找到。因此,与其包含 cstdio
并使用 std::fopen
,不如简单地包含 boost/nowide/cstdio.hpp
并使用 boost::nowide::fopen
。这些函数接受与其 std
对应项相同的参数,事实上,在非 Windows 构建上,它们只是这些函数的别名。但在 Windows 上,Boost.Nowide 会发挥其魔力:窄字符串参数被解释为 UTF-8,转换为宽字符串 (UTF-16),并传递给正确处理特殊字符的宽 API。
如果传递的字符串中存在非 UTF-8 字符,则转换会将它们替换为替换字符(默认:U+FFFD
),类似于 NT 内核的做法。这意味着无效的 UTF-8 序列不会从窄->宽->窄往返,导致例如如果文件名格式错误则无法打开文件。
为什么不提供宽字符和窄字符实现,以便开发人员可以选择在类 Unix 平台上使用宽字符?
有几个原因
wchar_t
并非真正可移植,它可以是 2 个字节、4 个字节甚至是 1 个字节,这使得 Unicode 感知编程更加困难fopen(const wchar_t*, const wchar_t*)
,因此最好坚持标准,而不是以“Microsoft Windows 样式”重新实现 Wide API自 2019 年 5 月的更新以来,Windows 10 通过清单文件支持窄字符串的 UTF-8。因此,将“UTF-8”设置为活动代码页将允许使用窄 API,而无需对 UTF-8 编码的字符串进行任何其他更改。有关详细信息,请参阅文档。
自 2018 年 4 月起,Windows 10 中提供了一个(Beta)功能,可以通过用户设置默认使用 UTF-8 代码页。
这两种方法都有效,但有一个主要的缺点:对于应用程序开发人员来说,它们并非完全可靠。通过清单方法的代码页在使用 1903 之前的旧版本 Windows 时会回退到旧代码页。因此,只有在目标系统是 2019 年 5 月之后的 Windows 10 时才能使用它。
第二种方法依赖于在启动程序之前进行用户交互。显然,当期望代码中只有 UTF-8 时,这是不可靠的。
此外,自 Windows 10 1803(即自 2018 年 4 月起)以来,可以使用诸如 setlocale(LC_ALL, ".UTF8");
等方法以编程方式将当前代码页设置为 UTF-8。这使得许多函数接受或生成 UTF-8 编码的字符串,这对于 std::filesystem::path
及其 string()
函数尤其有用。有关详细信息,请参阅文档。虽然这适用于大多数函数,但它不适用于例如程序参数(main
的 argv
和 env
参数)。
因此,在某些情况下(并希望在未来的某个时候),将不需要此库,即使 Windows I/O 也可以使用 UTF-8 编码的文本。
Boost.Nowide 通常通过 b2
作为 Boost 的一部分构建。
它需要 C++11 功能,如果缺少任何功能,则该库将不会被构建。
如果发生意外情况,请查看配置检查输出,其中是否有类似 cxx11_constexpr : no
的内容。
这意味着您的编译器不使用 C++11(或更高版本),例如因为它默认使用 C++03。您可以将 cxxstd=11
传递给 b2
以在 C++11 模式下进行构建。
还提供了使用 Boost CMake 构建系统进行构建的实验性支持。
为此,请运行例如 cmake -DBOOST_INCLUDE_LIBRARIES=nowide <boost-root-path> <other options>
。
作为开发人员,您应该使用 boost::nowide
函数,而不是 std
命名空间中提供的函数。
例如,这是行计数器的不识别 Unicode 的实现
为了使此程序正确处理 Unicode,我们进行以下更改
这种非常简单明了的方法有助于编写 Unicode 感知程序。
请注意 boost::nowide::args
、boost::nowide::ifstream
和 boost::nowide::cerr/cout
的使用。在非 Windows 上,它不会执行任何操作,但在 Windows 上,会发生以下情况
boost::nowide::args
从 Windows API 检索 UTF-16 参数,将其转换为 UTF-8,并在实例的生命周期内将原始 argv
(和可选的 env
)临时替换为指向那些内部存储的 UTF-8 字符串的指针。boost::nowide::ifstream
将传递的文件名(现在是有效的 UTF-8)转换为 UTF-16,并调用 Windows Wide API 来打开文件流,然后可以像往常一样使用该文件流。boost::nowide::cerr
和 boost::nowide::cout
使用底层流缓冲区,该缓冲区将 UTF-8 字符串转换为 UTF-16,并使用另一个 Wide API 函数将其写入控制台。当然,这组简单的函数并不能满足所有需求。如果需要从内部使用 UTF-8 的 Windows 应用程序访问 Wide API,可以使用函数 boost::nowide::widen
和 boost::nowide::narrow
。
例如
转换在最后阶段完成,您可以在其他所有地方继续使用 UTF-8 字符串。您只在粘合点切换到 Wide API。
boost::nowide::widen
返回 std::string
。有时,防止分配并使用堆栈缓冲区会很有用。Boost.Nowide 为此目的提供了 boost::nowide::basic_stackstring
类。
上面的例子可以重写为
stackstring
和 wstackstring
使用 256 个字符的缓冲区,short_stackstring
和 wshort_stackstring
使用 16 个字符的缓冲区。如果字符串更长,它们会回退到堆内存分配。该库不包含 windows.h
,以防止命名空间被大量的定义和类型污染。相反,该库定义了 Win32 API 函数的原型。
但是,您可以通过在包含任何 Boost.Nowide 头文件之前定义 BOOST_USE_WINDOWS_H
来请求使用 windows.h
头文件
Boost.Filesystem 支持选择窄编码。不幸的是,Windows 上的默认窄编码不是 UTF-8。但是,您可以通过在程序开始时调用 boost::nowide::nowide_filesystem()
来启用 UTF-8 作为 Boost.Filesystem 上的默认编码,这将使用一个带有 UTF-8 转换 facet 的区域设置来在 char
和 wchar_t
之间进行转换。当将所有传递给和来自 boost::filesystem::path
的窄字符串转换为宽字符串(内部存储所需)时,这会将它们解释为 UTF-8。在 POSIX 上,这通常没有影响,因为由于窄字符串用作存储格式,因此不进行转换。
对于自 C++17 起可用的 std::filesystem::path
,没有办法注入区域设置。但是,可以使用 u8string()
成员函数从 path
获取 UTF-8 编码的字符串。要从 UTF-8 编码的字符串中获取 path
,可以使用 std::filesystem::u8path
或自 C++20 起使用采用 char8_t
类型输入的 path
构造函数之一。
要将 std::filesystem::path
实例从/向流读取/写入,通常会使用例如 os << path
。但是,这实际上将作为 os << std::quoted(path.string())
运行,这意味着可能会转换为窄字符串,该字符串可能不是 UTF-8 编码的。为此,可以使用 quoted
对于 Microsoft Windows,该库在 boost::nowide
命名空间中提供了一些 std::
函数的 UTF-8 感知变体。例如,std::fopen
变为 boost::nowide::fopen
。
在 POSIX 平台下,boost::nowide
中的函数是其标准库对应项的别名
还为 Windows 提供了一个与 std::filebuf
兼容的实现,该实现支持 UTF-8 文件路径以进行 open
操作,并且在其他方面行为相同(在 API 方面)。
在所有系统上,std::fstream
类及其友元都作为自定义实现提供,这些实现支持 std::string
和 *::filesystem::path
以及用于构造函数和 open
的 wchar_t*
(仅限 Windows)重载。这样做是为了让用户可以使用例如 boost::filesystem::path
和 boost::nowide::fstream
,而不依赖于 C++17 支持。此外,如果任何类路径与 std::filesystem::path
的接口“足够”匹配,则支持它。
请注意,boost::nowide::filebuf
中没有对 path
和 std::string
的通用支持。这是由于在非 Windows 系统上使用 std 变体,在某些情况下可能会更快。由于 filebuf
很少被用户代码使用,而是间接地通过 fstream
使用,因此没有字符串或路径支持似乎代价很小,尤其是 C++11 添加了 std::string
支持,C++17 添加了 path
支持,并且仍然可以通过 string_or_path.c_str()
进行使用,它是可移植的。
当流转到“真实”控制台时,控制台 I/O 实现为 ReadConsoleW/WriteConsoleW 的包装器。当流被管道传输/重定向时,将改用标准 cin/cout
。
这种方法消除了手动代码页处理的需要。如果使用 TrueType 字体,则 Unicode 感知的输入和输出将按预期工作。
问:通过 Boost.Nowide 传递的无效 UTF 会发生什么?例如,Windows 使用 UCS-2 而不是 UTF-16。
答:Boost.Nowide 的策略是始终产生有效的 UTF 编码字符串。因此,无效的 UTF 字符将替换为替换字符 U+FFFD
。
这发生在两个方向上
当将(假定为)UTF-8 编码的字符串传递给 Boost.Nowide 时,它会将其转换为 UTF-16,并在将其传递给操作系统之前替换每个无效字符。
从操作系统检索值时(例如,boost::nowide::getenv
或通过 boost::nowide::args
的命令行参数),该值被假定为 UTF-16 并转换为 UTF-8,替换任何无效字符。
这意味着,如果有人设法在 Windows 中创建一个无效的 UTF-16 文件名,则不可能使用 Boost.Nowide 处理它。但是,由于 Microsoft 在 Windows 2000 中将 UCS-2(也称为具有任意 2 字节值的字符串)切换为 UTF-16,因此在大多数环境中这不是问题。
问:使用哪种错误报告?
答:实际上有 3 种
问:为什么该库不在 POSIX 系统上将字符串转换为/从区域设置的编码(而不是 UTF-8)转换?
答:在 POSIX 平台上,将字符串转换为/从区域设置编码是内在错误的。
您可以创建一个名为“\xFF\xFF.txt”(无效 UTF-8)的文件,删除它,将其名称作为参数传递给程序,无论当前区域设置是否为 UTF-8,它都会工作。此外,将区域设置从例如 en_US.UTF-8
更改为 en_US.ISO-8859-1
不会神奇地更改操作系统中的所有文件或用户可能传递给程序的字符串(这在 Windows 上是不同的)
POSIX 操作系统将字符串视为 NULL
终止的 cookie。
因此,根据区域设置更改其内容实际上会导致不正确的行为。
例如,这是标准程序“rm”的简单实现
它适用于任何区域设置,并且更改字符串会导致不正确的行为。
在 POSIX 和 Windows 平台下,locale 的含义不同,并且具有非常不同的效果。
可以使用 Nowide 库,而无需将庞大的 Boost 项目作为依赖项。有一个独立版本,它将所有功能放在 nowide
命名空间中,而不是 boost::nowide
。上面的例子将如下所示:
上游源代码可以在 GitHub 上找到:https://github.com/boostorg/nowide
您可以在那里下载最新的源代码