Boost C++ 库

...one of the most highly regarded and expertly designed C++ library projects in the world. Herb Sutter and Andrei Alexandrescu, C++ Coding Standards

Boost.Nowide: Boost.Nowide - Boost C++ 函数库
Boost.Nowide
Boost.Nowide

更新日志

目录

Boost.Nowide 是什么

Boost.Nowide 是 Artyom Beilis 最初实现的一个库,它使得跨平台 Unicode 感知编程更加容易。

该库提供了标准 C 和 C++ 库函数的实现,使其在 Windows 上的输入能够感知 UTF-8,而无需使用宽字符 API。在非 Windows/POSIX 平台上,StdLib 的等效函数被直接作为别名,因此不会执行转换,因为 UTF-8 已经普遍使用。

因此,您可以在所有平台上使用与 std 对应函数同名的 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-like 操作系统:Linux、Mac OS X、Solaris、BSD。但在 Microsoft Windows 上,对于像 War and Peace - Война и мир - מלחמה ושלום.zip 这样的文件,它将失败,因为原生的 Windows Unicode 感知 API 是宽字符 API——UTF-16。

这个极其微小的任务要以跨平台的方式实现非常困难。

解决方案

Boost.Nowide 提供了一组标准库函数,它们在 Windows 上支持 UTF-8,并使 Unicode 感知编程更加容易。

该库提供了:

  • 易于使用的 UTF-8 与 UTF-16 之间转换函数
  • 一个类,用于使 mainargcargcenv 参数使用 UTF-8
  • 支持 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 感知编程更加困难。
  • C 和 C++ 标准库在 OS 交互中使用窄字符串。该库遵循相同的通用规则。标准库中没有 fopen(const wchar_t*, const wchar_t*) 这样的函数,因此最好坚持标准,而不是用“Microsoft Windows 风格”重新实现宽字符 API。

替代方案

自 2019 年 5 月更新以来,Windows 10 通过清单文件支持窄字符串的 UTF-8。因此,将“UTF-8”设置为活动代码页可以在不进行任何其他更改的情况下,使用 UTF-8 编码的字符串来使用窄字符 API。有关详细信息,请参阅文档

自 2018 年 4 月起,Windows 10 中提供了一个(Beta 版)函数,可以通过用户设置使用 UTF-8 代码页作为默认设置。

这两种方法都可以使用,但有一个主要缺点:它们对于应用程序开发人员来说并不完全可靠。清单方法通过代码页在使用的 Windows 版本早于 1903 时会回退到旧代码页。因此,只有在目标系统是 2019 年 5 月之后的 Windows 10 时才能使用。
第二种方法依赖于程序启动之前的用户交互。显然,当只期望代码中使用 UTF-8 时,这种方法并不可靠。

此外,自 Windows 10 1803(即 2018 年 4 月起)以来,可以通过编程方式将当前代码页设置为 UTF-8,例如使用 setlocale(LC_ALL, ".UTF8");。这使得许多函数接受或生成 UTF-8 编码的字符串,这对于 std::filesystem::path 及其 string() 函数特别有用。有关详细信息,请参阅文档。虽然这适用于大多数函数,但对于程序参数(mainargvenv 参数)则不适用。

因此,在某些情况下(并希望在未来的某个时候始终如此),将不再需要此库,甚至 Windows I/O 也可以与 UTF-8 编码的文本一起使用。

深入阅读

使用库

构建库

Boost.Nowide 通常作为 Boost 的一部分通过 b2 构建。
它需要 C++11 功能,如果缺少任何功能,库将**不会**被构建。

如果发生这种情况,请检查配置检查输出,查找类似 cxx11_constexpr : no 的内容。
这意味着您的编译器不使用 C++11(或更高版本),例如,因为它默认使用 C++03。您可以将 cxxstd=11 传递给 b2 以在 C++11 模式下构建。

还提供了使用 Boost CMake 构建系统的实验性支持。
为此,例如运行 cmake -DBOOST_INCLUDE_LIBRARIES=nowide <path-to-boost-root> <other options>

标准功能

作为开发人员,您应该使用 boost::nowide 函数而不是 std 命名空间中可用的函数。

例如,这是一个不区分 Unicode 的行计数器实现:

#include <fstream>
#include <iostream>
int main(int argc,char **argv)
{
if(argc!=2) {
std::cerr << "Usage: file_name" << std::endl;
返回 1;
}
std::ifstream f(argv[1]);
if(!f) {
std::cerr << "Can't open " << argv[1] << std::endl;
返回 1;
}
int total_lines = 0;
while(f) {
if(f.get() == '\n')
total_lines++;
}
f.close();
std::cout << "File " << argv[1] << " has " << total_lines << " lines" << std::endl;
返回 0;
}

要使此程序正确处理 Unicode,我们进行以下更改:

#include <boost/nowide/args.hpp>
#include <boost/nowide/fstream.hpp>
#include <boost/nowide/iostream.hpp>
int main(int argc,char **argv)
{
boost::nowide::args a(argc,argv); // Fix arguments - make them UTF-8
if(argc!=2) {
boost::nowide::cerr << "Usage: file_name" << std::endl; // Unicode aware console
返回 1;
}
boost::nowide::ifstream f(argv[1]); // argv[1] - is UTF-8
if(!f) {
// the console can display UTF-8
boost::nowide::cerr << "Can't open " << argv[1] << std::endl;
返回 1;
}
int total_lines = 0;
while(f) {
if(f.get() == '\n')
total_lines++;
}
f.close();
// the console can display UTF-8
boost::nowide::cout << "File " << argv[1] << " has " << total_lines << " lines" << std::endl;
返回 0;
}
args 是一个类,它临时替换标准 main() 函数的参数,使其等同于...
定义 args.hpp:57
basic_ifstream< char > ifstream
Definition fstream.hpp:248
detail::winconsole_ostream cerr
与 std::cerr 相同,但使用 UTF-8。
detail::winconsole_ostream cout
与 std::cout 相同,但使用 UTF-8。

这种非常简单直接的方法有助于编写 Unicode 感知的程序。

注意 boost::nowide::argsboost::nowide::ifstreamboost::nowide::cerr/cout 的使用。在非 Windows 上,它们不做任何事情,但在 Windows 上,会发生以下情况:

  • boost::nowide::args 从 Windows API 检索 UTF-16 参数,将其转换为 UTF-8,并在实例的生命周期内临时用指向这些内部存储的 UTF-8 字符串的指针替换原始 argv(以及可选的 env)。
  • boost::nowide::ifstream 将传入的文件名(现在是有效的 UTF-8)转换为 UTF-16,并调用 Windows 宽字符 API 来打开文件流,然后可以像往常一样使用该文件流。
  • 同样,boost::nowide::cerrboost::nowide::cout 使用底层流缓冲区,该缓冲区将 UTF-8 字符串转换为 UTF-16,并使用另一个宽字符 API 函数将其写入控制台。

自定义 API

当然,这组简单的函数并不能满足所有需求。如果您需要从使用 UTF-8 的 Windows 应用程序访问宽字符 API,您可以使用 boost::nowide::widenboost::nowide::narrow 函数。

例如:

CopyFileW( boost::nowide::widen(existing_file).c_str(),
boost::nowide::widen(new_file).c_str(),
TRUE);
wchar_t * widen(wchar_t *output, size_t output_size, const char *begin, const char *end)
Definition convert.hpp:48

转换在最后一个阶段完成,您在其他所有地方继续使用 UTF-8 字符串。您只在粘合点切换到宽字符 API。

boost::nowide::widen 返回 std::string。有时,为了避免分配并使用堆栈缓冲区,使用 `boost::nowide::basic_stackstring` 类会更有用。

上面的示例可以重写为:

boost::nowide::basic_stackstring<wchar_t,char,64> wexisting_file(existing_file), wnew_file(new_file);
CopyFileW(wexisting_file.c_str(), wnew_file.c_str(), TRUE);
允许从宽窄 UTF 源创建临时宽窄 UTF 字符串的类。
定义 stackstring.hpp:32
注意
有几个方便的类型别名:使用 256 字符缓冲区的 stackstringwstackstring,以及使用 16 字符缓冲区的 short_stackstringwshort_stackstring。如果字符串更长,它们将回退到堆内存分配。

windows.h 头文件

该库不包含 windows.h,以避免因大量宏定义和类型而导致命名空间污染。相反,该库定义了 Win32 API 函数的原型。

但是,您可以通过在包含任何 Boost.Nowide 头文件之前定义 BOOST_USE_WINDOWS_H 来请求使用 windows.h 头文件。

与 Boost.Filesystem 集成

Boost.Filesystem 支持选择窄字符编码。不幸的是,Windows 上的默认窄字符编码不是 UTF-8。但是,您可以通过在程序开头调用 boost::nowide::nowide_filesystem() 来启用 UTF-8 作为 Boost.Filesystem 的默认编码,该函数会注入一个带有 UTF-8 转换 facet 的 locale,用于在 charwchar_t 之间进行转换。这会将传递给 boost::filesystem::path 的所有窄字符串以及从其获得的窄字符串解释为 UTF-8,在将其转换为宽字符串时(这是内部存储所必需的)。在 POSIX 上,这通常没有影响,因为由于窄字符串被用作存储格式,因此不会进行任何转换。

对于 C++17 起可用的 std::filesystem::path,没有办法注入 locale。但是,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

#include <boost/nowide/quoted.hpp>
#include <filesystem>
#include <sstream>
std::string write(const std::filesystem::path& path)
{
std::ostringstream s;
s << boost::nowide::quoted(path);
return s.str();
}
std::experimental::path read(std::istream& is)
{
std::filesystem::path path;
is >> boost::nowide::quoted(path);
return path;
}

技术细节

Windows vs POSIX

对于 Microsoft Windows,该库在 boost::nowide 命名空间中提供了 std: 一些函数的 UTF-8 感知变体。例如,std::fopen 变成 boost::nowide::fopen

在 POSIX 平台上,boost::nowide 中的函数是其标准库对应函数的别名。

namespace boost {
namespace nowide {
#ifdef BOOST_WINDOWS
inline FILE *fopen(const char* name, const char* mode)
{
...
}
#else
using std::fopen
#endif
} // nowide
} // boost
FILE * fopen(const char *file_name, const char *mode)
与 fopen 相同,但 file_name 和 mode 是 UTF-8 字符串。

对于 Windows,还提供了一个与 std::filebuf 兼容的实现,它支持 UTF-8 文件路径的 open,并且在 API 方面行为完全相同。

在所有系统上,std::fstream 类及其朋友都被提供为自定义实现,它们支持 std::string*::filesystem::path 以及 wchar_t*(仅限 Windows)的构造函数和 open 重载。这是为了让用户可以使用例如 boost::filesystem::pathboost::nowide::fstream,而无需依赖 C++17 支持。此外,任何类路径的对象都可以支持,只要它“足够”匹配 std::filesystem::path 的接口。

请注意,boost::nowide::filebuf 并不普遍支持 pathstd::string。这是因为在非 Windows 系统上使用了 std 变体,而在某些情况下可能更快。由于用户代码很少直接使用 filebuf,而是间接通过 fstream 使用,因此不支持字符串或路径似乎是一个小代价,尤其是在 C++11 添加了 std::string 支持,C++17 添加了 path 支持,并且仍然可以通过 string_or_path.c_str() 进行移植性使用。

控制台 I/O

控制台 I/O 实现为 ReadConsoleW/WriteConsoleW 的包装器,当流指向“真实”控制台时。当流被管道/重定向时,将使用标准的 cin/cout

这种方法消除了手动处理代码页的需要。如果使用 TrueType 字体,Unicode 感知的输入和输出将按预期工作。

问答

问:无效的 UTF 字符通过 Boost.Nowide 会发生什么?例如,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 从 UCS-2(又名任意 2 字节值的字符串)切换到 Windows 2000 中的 UTF-16,因此在大多数环境中这不会成为问题。

问:使用哪种类型的错误报告?

答:实际上有 3 种:

  • 使用无效 UTF 编码的字符串,通过将无效字符替换为替换字符 U+FFFD。
  • 镜像标准 API 的 API 调用使用与该 API 相同的错误报告,例如,在失败时返回非零值。
  • 不可继续的错误通过标准异常报告。主要示例是通过 WinAPI 获取命令行参数的失败。

问:为什么库不在 POSIX 系统上将字符串转换为/从 locale 的编码(而不是 UTF-8)?

答:在 POSIX 平台上将字符串转换为/从 locale 编码进行转换本质上是不正确的。

您可以创建一个名为“\xFF\xFF.txt”的文件(无效 UTF-8),删除它,将文件名作为参数传递给程序,无论当前 locale 是 UTF-8 还是其他,它都会正常工作。此外,将 locale 从例如 en_US.UTF-8 更改为 en_US.ISO-8859-1 不会神奇地更改 OS 中的所有文件或用户可能传递给程序的字符串(这与 Windows 不同)。

POSIX OS 将字符串视为 `NULL` 终止的“cookie”。

因此,根据 locale 修改它们的字符串内容实际上会导致错误的行为。

例如,这是标准程序“rm”的一个朴素实现:

#include <cstdio>
int main(int argc,char **argv)
{
for(int i=1;i<argc;i++)
std::remove(argv[i]);
返回 0;
}

它适用于**任何** locale,更改字符串会导致错误行为。

POSIX 和 Windows 平台上的 locale 的含义不同,并且具有非常不同的影响。

独立版本

无需将庞大的 Boost 项目作为依赖项即可使用 Nowide 库。有一个独立版本,它将所有功能都放在 nowide 命名空间中,而不是 boost::nowide。上面的示例将如下所示:

#include <nowide/args.hpp>
#include <nowide/fstream.hpp>
#include <nowide/iostream.hpp>
int main(int argc,char **argv)
{
nowide::args a(argc,argv); // Fix arguments - make them UTF-8
if(argc!=2) {
nowide::cerr << "Usage: file_name" << std::endl; // Unicode aware console
返回 1;
}
nowide::ifstream f(argv[1]); // argv[1] - is UTF-8
if(!f) {
// the console can display UTF-8
nowide::cerr << "Can't open a file " << argv[1] << std::endl;
返回 1;
}
int total_lines = 0;
while(f) {
if(f.get() == '\n')
total_lines++;
}
f.close();
// the console can display UTF-8
nowide::cout << "File " << argv[1] << " has " << total_lines << " lines" << std::endl;
返回 0;
}

源代码和下载

上游源代码可以在 GitHub 上找到:https://github.com/boostorg/nowide

您可以在那里下载最新的源代码。