如何在 C++ 中使用 UTF-8 和 Unicode? C++20 char8_t 有多大?

Posted

技术标签:

【中文标题】如何在 C++ 中使用 UTF-8 和 Unicode? C++20 char8_t 有多大?【英文标题】:How to use UTF-8 and Unicode in C++? How big is C++20 char8_t? 【发布时间】:2020-09-10 15:08:14 【问题描述】:

假设我想在 C++ 中存储一个(而不是 std::string 中的)Unicode 字符,我该怎么做? char8_t 是在 C++20 中引入的,但看起来它只是 unsigned char 的 typedef,最多只能存储 1 个字节的信息。某些字符(尤其是表情符号等更具异国情调的字符)一次最多可占用 4 个字节。

不起作用的代码示例:

char8_t smth = "????";

有趣的是,尽管sizeof() 说它有 8 个字节大,但我对此表示怀疑。

const char* smth = "????";

【问题讨论】:

char32_t 可以存储任何 Unicode 字符。试试char32_t smth = U'????';(注意 U 前缀和单引号)。如果要使用 UTF-8 编码,则需要将此类字符存储为字符串(8 位字符)。 sizeof 描述了指针的大小,它当然可以是 8 个字节大。如果您想要字符串的长度(以字节为单位),请使用strlen。虽然要小心,因为 UTF-8 序列可以包含 nul 字节,这会欺骗strlen const char* 是一个指针,在 64 位机器上可能是 8 个字节,所以这是正确的。 @IgorTandetnik 我认为 char8_t 的意义在于拥有一种可以使用 UTF-8 编码表示所有 Unicode 字符的类型。 @Xrey274 这不是char8_t 的用途。它表示 single 编码 single Unicode codepoint 的 codeunits sequence 中的 single UTF-8 codeunit 。 (对于 UTF-16 与 char16_t 相同,对于 UTF-32 与 char32_t 相同)。在 UTF-8 中,???? 被编码为 4 个代码单元(字节)F0 9F 98 80。您可以使用const char smth[] = u8"????"; int size = sizeof(smyh) - 1; 获取该号码。一些表情符号需要多个 代码点,因此会超过 4 个字节。 【参考方案1】:

Unicode vs UTF-8 vs UTF-32 vs char8_t vs char32_t

Unicode 是基于 32 位无符号整数表示的标准字符表示(code point)。通过滥用语言我们也说“Unicode”来谈论代码点。例如 ? 的 Unicode(代码点)是 0x1F600

UTF-32 是将 Unicode 代码点简单地编码为 4 个字节(或 32 位)。这很简单,因为您可以只存储 32 位无符号整数的代码点。

UTF-8 是一种 Unicode 代码点的编码格式,能够将它们存储在 1 到 4 个 8 位数据块中。这是可能的,因为 Unicode 代码点不使用所有 32 位,因此可以用 1 个字节(或 8 位)表示最常用的字符(~ASCII),用 2 到 4 个字节表示不常用的字符。

char8_t 大致是一个 8 位的无符号整数。我说“大致”是因为(至少)两个原因:第一个 c++ 标准规定它的大小至少为 8 位,但如果编译器/系统决定这样做可能会更多,其次它被认为是它的唯一类型并且不是' 与 std::uint8_t 不完全相同(尽管从一个转换到另一个是微不足道的)。

char32_t 类似于char8_t,除了它允许使用 32 位(因此它与std::uint32_t 大致相当),这很方便,因为您可以使用它来存储一个 Unicode 代码点。

char(8_t) const*的案例

在 C++ 中,使用 c-string (char(8_t) const*) 时应该小心。它们的行为不像一个对象,而是像一个指针,因此查询它的大小将返回指针之一(在 64 位处理器上为 8)。下面的代码看起来更愚蠢:

char8_t const* str = u"Hello";
sizeof(str); // == 8
sizeof(u"Hello"); // == 6 (5 letters + trailing 0x00)

使用适当的字符串文字

使用char(或char const*std::string)时要小心。它不是用来存储 UTF-8 编码的字符串,而是存储扩展的 ASCII。因此,您的编译器将不知道您正在尝试编写什么,并且可能不会按照您的预期进行。

char c0 = '?';             // = '?' on Visual Studio (with 3 warnings)
char8_t c1 = u8'?';        // Compilation error: trying to store 4 char8_t in 1
char32_t c2 = U'?';        // = ? (or 128512)

char const* s0 = "?";      // = "??" on Visual Studio (with 1 warning)
char8_t const* s1 = "?";   // = "?" stored on 4 bytes (0xf0, 0x9f, 0x98, 0x80), or "😀"
char32_t const* s2 = U"?"; // = "?" stored like the 4 bytes unsigned integer 128512

sizeof("?");               // = 3: 2 bytes for ? (not sure why) + 1 byte for 0x00
sizeof(u8"?");             // = 5: 4 bytes for ? + 1 byte for 0x00
sizeof(U"?");              // = 8: 4 bytes for ? + 4 bytes for 0x00

存储一个 Unicode/Unicode 字符

正如 Igor 所说,存储 1 个 Unicode 字符 可以通过使用 char32_t 来完成。但是,如果您想存储代码本身(整数),您可以存储 std::uint32_t。这两种表示对于编译器和语义都是不同的,所以请注意!大多数时候使用 char32_t 会更明确,更不容易出错。

char32_t c = U'?';
std::uint32_t u = 0x1F600u; // it's funny because 'u' stands for unsigned here..

存储一串 Unicode 字符

但是,如果您想存储一串 Unicode 字符,您有多种选择。你首先想知道的是你的程序的约束是什么,它与什么其他系统交互等等。

    使用 char32_t

如果您需要不断添加/删除字符或检查 Unicode(例如,如果您需要从字体在屏幕上绘制字符)并且 - 这非常重要 - 如果您没有强大的内存限制 + 您不要与使用普通字符串存储UTF-8 字符的(旧)库交互,您可以通过使用char32_t 来使用UTF-32 表示:

std::size_t size = sizeof(U"?Ö"); // = 12 -> 4 bytes for each character including trailing 0x00

char32_t const* cString = U"?Ö"; // sizeof(...) = 8 -> the size of a pointer

std::u32string string U"?Ö" ; // .size() = 2

std::u32string_view stringView U"?Ö" ; // .size() = 2
    使用 char8_t

如果您受到内存的限制并且无法为每个 Unicode 使用 32 位存储空间(知道在大多数情况下它将是 ASCII 字符,在 UTF-8 编码中只能用 8 位表示)或者如果您需要与(例如)使用char const*/std::string 来存储UTF-8 编码字符的库进行交互,您可以决定通过使用char8_t 来存储以UTF-8 编码的字符串:

std::size_t size = sizeof(u8"?Ö");
// = 7 -> 4 bytes for the emoji (they are pretty uncommon so UTF-8 encodes them on 4 bytes)
//   + 2 bytes for the "Ö" (not as uncommon but not a -very common- ASCII)
//   + 1 byte for the trailing 0x00

char8_t const* cString = u8"?Ö"; // sizeof(...) = 8 -> the size of a pointer

std::u8string string u8"?Ö" ; // .size() = 6 (string's size method doesn't count the 0x00)

std::u8string_view stringView u8"?Ö" ; // .size() = 6

使用char8_t 的技巧是,从技术上讲,您的计算机不知道它是用UTF-8 编码的(好吧,您的编译器会知道并为您编码“?Ö”),它只知道您正在存储代表字符的 8 位长的东西,因此为什么当您询问这些字符串的大小时它不会返回“2”。如果您需要知道代表多少个 Unicode(或者您必须在屏幕上绘制多少个字符),您需要对此编码进行解码。它可能存在一些可以为你做的花哨的库,但这是我个人使用的(我根据 UTF-8 规范编写的):

// How many char8_t of this string you need to read to get 1 Unicode. The trick here 
// is that it can be done using only the first char8_t of the string because of how
// UTF-8 encoding works. However this won't check for following bytes that could be
// erroneous.
constexpr std::size_t code_size(std::u8string_view a_string) noexcept

    auto const h0 = a_string[0] & 0b11110000;
    return h0 < 0b10000000 ? 1 : (h0 < 0b11100000 ? 2 : (h0 < 0b11110000 ? 3 : 4));


// How many char8_t you need to add to a string to encode this Unicode with UTF-8.
constexpr std::size_t code_size(char32_t const a_code) noexcept

    return a_code < 0x007f ? 1 : (a_code < 0x07ff ? 2 : (a_code < 0xffff ? 3 : 4));


// How many Unicode characters are stored in this UTF-8 encoded string.
constexpr std::size_t string_size(std::u8string_view a_string) noexcept

    auto size = 0ull;
    while (!a_string.empty())
    
        auto const codeSize = code_size(a_string);
        if (codeSize > a_string.size())
        
            return -1; // Error: this is not a valid UTF-8 encoded string.
        
        size += codeSize;
        a_string = a_string.substr(codeSize);
    


// Append the UTF-8 encoding of a code to an u8string.
template<typename TAllocator>
constexpr std::size_t write(
    char32_t a_code,
    std::basic_string<char8_t, std::char_traits<char8_t>, TAllocator>& a_output) noexcept

    if (a_code <= 0x007f)
    
        a_output += static_cast<char8_t>(a_code);
        return 1;
    
    else if (a_code <= 0x07ff)
    
        a_output += static_cast<char8_t>(0b11000000 | ((a_code >> 6) & 0b00011111));
        a_output += static_cast<char8_t>(0b10000000 | (a_code & 0b00111111));
        return 2;
    
    else if (a_code <= 0xffff)
    
        a_output += static_cast<char8_t>(0b11100000 | ((a_code >> 12) & 0b00001111));
        a_output += static_cast<char8_t>(0b10000000 | ((a_code >> 6) & 0b00111111));
        a_output += static_cast<char8_t>(0b10000000 | (a_code & 0b00111111));
        return 3;
    
    else
    
        a_output += static_cast<char8_t>(0b11110000 | ((a_code >> 18) & 0b00000111));
        a_output += static_cast<char8_t>(0b10000000 | ((a_code >> 12) & 0b00111111));
        a_output += static_cast<char8_t>(0b10000000 | ((a_code >> 6) & 0b00111111));
        a_output += static_cast<char8_t>(0b10000000 | (a_code & 0b00111111));
        return 4;
    


// Read an Unicode from an UTF-8 encoded string view, effectively decreasing its size.
constexpr char32_t read(std::u8string_view& a_string)

    if (a_string.empty())
    
        return 0x0000; // Null character
    

    auto const codeSize = code_size(a_string);
    if (codeSize > a_string.size())
    
        return 0xffff; // Invalid unicode
    

    char8_t mask0 = codeSize < 2 ?
        0b1111111 : (codeSize < 3 ? 0b11111 : (codeSize < 4 ? 0b1111 : 0b111));
    char32_t unicode = mask0 & a_string[0];
    a_string = a_string.substr(1);

    constexpr char8_t mask = 0b00111111;
    for (auto i = 1u; i < codeSize; ++i)
    
        if ((a_string[0] & ~mask) != 0b10000000)
        
            return 0xffff; // Invalid unicode
        
        unicode = (unicode << 6) | (mask & a_string[0]);
        a_string = a_string.substr(1);
    
    
    return unicode;

【讨论】:

以上是关于如何在 C++ 中使用 UTF-8 和 Unicode? C++20 char8_t 有多大?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 C++ 中使用 UTF-8 和 Unicode? C++20 char8_t 有多大?

关于编码

utf-8 German Umlaut有两种不同的字节码表示

encode 和 decode

如何使用 C++ 将 ISO-2022-KR 编码转换为 UTF-8 编码?

Python 字符集编码 - UTF-8 编码