如何在 Unicode/UCS 代码点和 UTF16 代理对之间进行转换?

jot*_*tik 0 c++ unicode ucs surrogate-pairs c++14

如何在 C++14 及更高版本中的 Unicode/UCS 代码点和 UTF16 代理对之间来回转换?

编辑:删除了 UCS-2 代理的提及,因为没有这样的事情。谢谢@remy-lebeau

jot*_*tik 7

标签信息页解释(优于由指定的Unicode标准9.0在§3.9,表3-5)。该算法从码点转换到代理对如下:

基本多语言平面之外的 Unicode 字符,即代码高于 0xFFFF 的字符,通过称为代理对的 16 位代码单元对以 UTF-16 编码,采用以下方案:

  • 从代码点中减去0x010000,留下0..0x0FFFFF范围内的20位数字;
  • 前十位(范围为 0..0x03FF 的数字)添加到 0xD800 以给出第一个代码单元或高代理,其范围为 0xD800..0xDBFF;
  • 将低十位(也在 0..0x03FF 范围内)添加到 0xDC00 以提供第二个代码单元或低代理,其将在 0xDC00..0xDFFF 范围内。

在 C++14 及更高版本中,这可以写为:

#include <cstdint>

using codepoint = std::uint32_t;
using utf16 = std::uint16_t;

struct surrogate {
    utf16 high; // Leading
    utf16 low;  // Trailing
};

constexpr surrogate split(codepoint const in) noexcept {
    auto const inMinus0x10000 = (in - 0x10000);
    surrogate const r{
            static_cast<utf16>((inMinus0x10000 / 0x400) + 0xd800), // High
            static_cast<utf16>((inMinus0x10000 % 0x400) + 0xdc00)}; // Low
    return r;
}
Run Code Online (Sandbox Code Playgroud)

在相反的方向上,只需将高代理的最后 10 位和低代理的最后 10 位组合起来,然后添加0x10000

constexpr codepoint combine(surrogate const s) noexcept {
    return static_cast<codepoint>(
            ((s.high - 0xd800) * 0x400) + (s.low - 0xdc00) + 0x10000);
}
Run Code Online (Sandbox Code Playgroud)

这是对这些转换的测试:

#include <cassert>

constexpr bool isValidUtf16Surrogate(utf16 v) noexcept
{ return (v & 0xf800) == 0xd800; }

constexpr bool isValidCodePoint(codepoint v) noexcept {
    return (v <= 0x10ffff)
        && ((v >= 0x10000) || !isValidUtf16Surrogate(static_cast<utf16>(v)));
}

constexpr bool isValidUtf16HighSurrogate(utf16 v) noexcept
{ return (v & 0xfc00) == 0xd800; }

constexpr bool isValidUtf16LowSurrogate(utf16 v) noexcept
{ return (v & 0xfc00) == 0xdc00; }

constexpr bool codePointNeedsUtf16Surrogates(codepoint v) noexcept
{ return (v >= 0x10000) && (v <= 0x10ffff); }

void test(codepoint const in) {
    assert(isValidCodePoint(in));
    assert(codePointNeedsUtf16Surrogates(in));
    auto const s = split(in);
    assert(isValidUtf16HighSurrogate(s.high));
    assert(isValidUtf16LowSurrogate(s.low));
    auto const out = combine(s);
    assert(isValidCodePoint(out));
    assert(in == out);
}

int main() {
    for (codepoint c = 0x10000; c <= 0x10ffff; ++c)
        test(c);
}
Run Code Online (Sandbox Code Playgroud)


Rem*_*eau 5

在 C++11 及更高版本中,您可以使用std::wstring_convert以下std::codecvt类型在各种 UTF/UCS 编码之间进行转换:

您不需要手动处理代理。

您可以std::u32string用来保存您的代码点,以及std::u16string保存您的 UTF-16/UCS-2 代码单元。

例如:

using convert_utf16_uf32 = std::wstring_convert<std::codecvt_utf16<char32_t>, char16_t>;

std::u16string CodepointToUTF16(const char32_t codepoint)
{
    const char32_t *p = &codepoint;
    return convert_utf16_uf32{}.from_bytes(
        reinterpret_cast<const char*>(p),
        reinterpret_cast<const char*>(p+1)
    );
}

std::u16string UTF32toUTF16(const std::u32string &str)
{
    return convert_utf16_uf32{}.from_bytes(
        reinterpret_cast<const char*>(str.data()),
        reinterpret_cast<const char*>(str.data()+str.size())
    );
}

char32_t UTF16toCodepoint(const std::u16string &str)
{
    std::string bytes = convert_utf16_uf32{}.to_bytes(str);
    return *(reinterpret_cast<const char32_t*>(bytes.data()));
}

std::u32string UTF16toUTF32(const std::u16string &str)
{
    std::string bytes = convert_utf16_uf32{}.to_bytes(str);
    return std::u32string(
       reinterpret_cast<const char32_t*>(bytes.data()),
       bytes.size() / sizeof(char32_t)
    );
}
Run Code Online (Sandbox Code Playgroud)

  • 此外,根据 cppreference.com,`std::codecvt_` 前缀类似乎在 C++17 中已被弃用。对此有何评论? (2认同)