C++ – 使用标准库刻面进行字符集转换

在 C++ STL 中包含多种不同的字符转换方式,主要分为继承自 C 风格函数式转换和 C++ 刻面对象。

本地环境和刻面

本地环境是一系列刻面的集合,描述了本地字符的编码,刻面描述了各种文本处理的方案。

例如在默认的 "C" 本地环境中,本地窄多字节编码指的是 ASCII 码,而在 "Chinese (Simplified)_China.936" 本地环境中,本地窄多字节编码指的是 GBK 码。在 "en_US.UTF-8" 本地环境中, . 被数值分析刻面视为小数点,而在 "de_DE.UTF-8" 本地换中, , 被数值分析刻面视为小数点。

需要注意的是, C 语言全局本地环境与 C++ 全局本地环境可能是互相独立的不同的。

字符类型

首先为了便于理解,首先要对 C++ 中的字符类型有一个了解。

C++ 中包含 charwchar_tchar8_tchar16_tchar32_t 这几种字符类型。

在最新的 C++ 标准中,我们可以假定 char 类型代表的是本地窄多字节编码,wchar_t 代表的是本地宽多字节编码,而他们的具体编码依赖于本地环境。而其他三种类型分别代表 UTF-8 、 UTF-16 和 UTF-32 编码,不依赖于本地环境。

所以通常情况下,函数可以通过参数类型或者模版类型参数来定义要处理的编码。

然而因为历史遗留原因, char8_t 也就是专用于代表 UTF-8 编码的类型是在 C++20 才加入标准的,但是 UTF-8 编码的字面量在 C++11 就已经被加入标准并且允许直接赋值给 char ,所以 char 在很长一段时间里被允许持有 UTF-8 编码的字符,这正是混乱的开始。

因为上述原因,在 C++20 之前,部分标准库工具将 char 视为本地窄多字节编码,而另一部分将其视为 UTF-8 编码。在 C++20 之后,为了兼容性,这部分标准库工具虽然被标记为弃用,但还是留了下来并占用一个位置,导致存在不符合 C++20 预期的行为。

C 风格函数式转换

在 C 本地环境中,本地环境通过状态机的方式管理,也就是只存在一个全局本地环境,可以通过 std::setlocale 函数修改,修改完成后,所有依赖本地环境的函数将以新的本地环境描述文本,例如 std::printf 等。同时全局状态机意味着无法在多线程环境使用。

这里简单介绍一下 C 语言的函数式转换,主要文档在 这里

首先是本地编码 charwchar_t 之间的转换,使用的是 std::mbsrtowcsstd::wcsrtombs 函数,在实际测试中,他们会将 char 视为本地窄多字节编码并完成正确转换,会受到全局本地环境的影响。

但是对于 Unicode 编码,在 C++11 加入的 std::mbrtoc16std::c16rtombstd::mbrtoc32std::c32rtomb 会将其参数的 char 类型视为 UTF-8 编码,而不是字面意义的本地窄多字节编码,并不会受到全局本地环境的影响。即便是在 C++20 后也是如此。

对于 C++20 加入的 std::mbrtoc8std::c8rtomb ,显然他们参数的 char 类型代表本地窄多字节编码,但是并没有编译器真正实现他们。也就是说 C 风格函数式转换并不能完成 UTF-8 和本地环境之间的转换。

C++ 刻面对象

在 C++ 本地环境中,本地环境被定义为一个类型 std::locale ,而用户可以创建多个独立的本地环境对象,这样在使用依赖本地环境的工具时,可以直接传递本地环境对象而不需要修改全局本地环境。这样就可以在多线程环境下使用了。同时 C++ 也提供了通过对象修改全局本地环境的接口 std::locale::global

而每一类本地对象都持有一个刻面集合,这样当我们需要进行某项文本处理的时候只需要通过刻面对象拿到对应的刻面对象即可。我们可以通过 std::has_facet 接口查询一个本地环境是否实现了所需刻面,并通过 std::use_facet 从本地环境中得到对应刻面的指针。当然我们也可以定义自己的刻面并加入某个本地环境中,这里不展开,只需要知道 std::use_facet 拿到的是刻面实现类的父指针,而我们实际上是通过虚函数多态调用具体实现。

用于编码转换的刻面是 std::codecvt ,其中前两个参数是需要转换的编码。标准库会确保 std::codecvt<char, char, std::mbstate_t> 即恒等转换刻面和 std::codecvt<wchar_t, char, std::mbstate_t> 即本地窄多字节编码和本地宽多字节编码转换刻面是必须实现的。

正如前面所说的,虽然上面的两个必要实现中, char 表示的是本地窄多字节编码,但标准在 C++11 时将 std::codecvt<char16_t, char, std::mbstate_t>std::codecvt<char32_t, char, std::mbstate_t> 加入必须实现时,将 char 定义为 UTF-8 编码。就连在同一个工具的特化中, char 都出现了两种解释!不过在 C++20 中后者被标记为废弃。

下面是一个使用 std::codecvt<wchar_t, char, std::mbstate_t> 转换编码的例子。

// 定义输入和输出字符串
char    in_str[] = "Hello, world!";
wchar_t out_str[16];

// 定义迭代器 转换结束后将指向转换后的字符串结尾
const char* in_ptr  = in_str;
wchar_t*    out_ptr = out_str;

// 定义用户偏好本地环境 即系统默认的本地环境
std::locale loc = std::locale("");

// 获取本地窄多字节编码和本地宽多字节编码之间的转换方式 即取出刻面
const auto& facet = std::use_facet<std::codecvt<wchar_t, char, mbstate_t>>(loc);

// 定义转换状态 用于保存转换的中间状态 不过在这里并没有用到 因为转换是一次性的
std::mbstate_t State = std::mbstate_t();

// 执行转换
facet.in(State, in_str, in_str + std::strlen(in_str), in_ptr,
    out_str, out_str + sizeof(out_str), out_ptr);

关于平台支持方面, Windows 11 MSVC 的用户偏好本地环境实现了五种字符类型任意两两之间的转换刻面,但是因为标准在 C++11 时加入的 std::codecvt<char16_t, char, std::mbstate_t>std::codecvt<char32_t, char, std::mbstate_t> 刻面,导致在处理与 wchar_t 无关的转换刻面时, MSVC 被迫将 char 定义为 UTF-8 编码,导致大部分与 char 有关的刻面无法使用。而且 MSVC 的实现中,与 char32_t 相关的转换刻面在转非 Unicode 基本多文种平面的字符时可能出现返回为转换成功但实际不正确的情况,谨慎使用。

Linux GCC 的用户偏好本地环境只实现了标准要求的几个转换刻面,但转换结果可靠。

发表回复