C++ 中 Unicode 字符串的宽度

首先,什么是 Unicode?

Unicode 实际上是一个统一的文字编码标准,它出现目的是为了解决不同计算机之间字符编码不同而导致的灾难性不兼容问题。

Unicode 字符集与 Unicode 编码是两种不同的概念。Unicode 字符集实际是对进入标准的所有文字用一个唯一的数字编码指代,就像用 1 指代字母 a,用 2 指代字母 b,并以此类推。在标准规范中,这里的数字被称为 Unicode Code Point,它一般都被写为 U+xxxx 的格式。

截至目前,Unicode Code Point 能被用 4 字节长的数值完全覆盖。

但受限于编解码识别和字符分配问题,Code Point 不会覆盖完整的 2 32 − 1 2^{32} - 1 2321 个字符,同时它的编码数字增长也不连续。

举例来说,CJK Unified Ideographs Extension 部分的 Code Point 在拓展集 B 到 I 之间就存在若干数值未被分配。

而 Unicode 编码则是对上述 Code Point 的再编码,也就是将 4 字节长的 Code Point 根据编码方案的不同压缩成不同的字节表示。

其中由于 Code Point 可以被 4 字节长的数值完全覆盖,所以 UTF-32(下称 U32)是编码到 Code Point 的直接映射;而 UTF-16(下称 U16)和 UTF-8(下称 U8)则是利用了不同字节前缀长进行了变长编码,当然最长不会超过 4 字节。

所以到这里读者应该能够看出,UTF-xx 后的数字就是这一编码方案要求的字符所占的二进制位个数。

而 U16 就是纯纯臭手,Unicode 设计时傲慢的洋人以为 16 位就能塞进所有字符了,所以一开始 U16 才是定长编码;没想到后来 CJK 字符集直接给大伙整不会了,于是设计 Unicode 的家伙出尔反尔让 U16 也变成了变长编码。

这一历史错误直接导致微软的 Windows C++ 底层字符编码基于 U16 而非 U32。

啊我草洋人怎么这么坏

Unicode Support in Cpp

C++ 的 Unicode 支持其实是一个老生常谈且经久不衰的问题了。

其实 Unicode 的支持可以分为两个部分:语言标识符的 Unicode 编码支持,以及 Unicode 字符串的支持;前者没什么好说,C++11 引入了一些与字符集编码相关的规范,并且从该标准后主流编译器都逐步开始支持 U8 等扩展字符集编码文本。

也就是说可以这样写代码:

#include <iostream>int main()
{auto 你好 = "Hello, world!";std::cout << 你好 << std::endl;
}

见过很多初学者试图这样写但是翻车了;往往这是因为代码文本的字符集编码与编译器假断的不同;对于 GCC/Clang 编译器,如果你的代码文件用的是 GBK 编码,可以使用命令行参数告知编译器用 GBK 规范解码 -finput-charset=GBK

但是如果要把字面量字符串输出到屏幕上,还需要告知编译器 -fexec-charset=GBK 以更改字符串编码格式,否则就会在终端上看到经典的中文乱码。

以上操作仅限于使用 GBK 编码的系统环境。

对于 Unicode 字符串就没这么好运了;C++11 后标准引入的 3 种 Unicode 编码字符串的字面量和字符类型,分别是:

auto utf16 = u"这是 UTF-16 编码字符串"; // C++11
auto utf32 = U"这是 UTF-32 编码字符串"; // C++11
auto utf8  = u8"这是 UTF-8 编码字符串"; // C++20char16_t utf16_char = u'\u4F60';
char32_t utf32_char = U'\U0001F600';
char8_t  utf8_char  = u8'A';

这两个类型不能说毫无作用,只能说聊胜于无。一方面,标准库根据这两个类型提供了 std::u16stringstd::u32string 两种 UTF 编码的字符串类型;另一方面,标准库完全没有提供这几个类型的输入输出支持。

如果需要输出,还需要利用各种扭曲的类型转换将 char16_t 等编码的字符串当成 char 处理,也就是像下面这样:

#include <iostream>
#include <string>
using namespace std;int main()
{
#ifdef _WIN32system( "chcp 65001" );
#endif // 65001 开启的是终端环境的 U8 字符集支持// 所以这里要用 C++20 的 std::u8stringauto u8str = std::u8string( u8"Coding in UTF-8" );std::cout << reinterpret_cast<const char*>( u8str.data() ) << std::endl;
}

当然这几个类型也不是一无是处,它们唯一的优点就是:存储在字面量文本中的字符编码不会受到 -fexec-charset 等编译开关的影响,而是始终保持着指定的 Unicode 编码。换句话说,指针 char8_t* 指向的字符串一定是 U8 编码的字符串,但 char* 指向的字符串的编码格式只有天知道。

在 C++98 时就已经出现的 wchar_t 比起上面这两个还稍微有一点用,至少它活跃在 Windows 的底层 API 中;这个类型的大小在不同平台上是可变的(Windows 上 2 字节,Linux 及非 Windows 平台则普遍为 4 字节)。

而标准曾经还要求这个类型必须大到足以容纳所有字符编码,但很显然在 U32 出现之后,这一目标不可能在 Windows 上实现。

并且这个类型存储的数据与 char 一样,是编码无关的。

Support Unicode in Cpp

虽然标准本身并不直接支持完整的 Unicode 编码方案,但其实如果要实现 UTF 编码字符串支持也不是很困难。

从使用上来说,实现 UTF 编码字符串支持的首要工作就是数清楚字符串里面有几个 UTF 编码字符;由于不同的变长编码都是基于字节寻址的,所以这个工作并不困难:我们只需要根据不同的编码前缀识别当前字节的长度,并逐字节扫描过去就能数清楚有几个编码字符了。

对于 U8 来说是这样的:

#include <cstdint>
#include <iostream>
#include <string_view>
#include <cassert>// 这里用了 C++17 的 std::string_view
std::size_t count_u8_char( std::string_view u8_str )
{std::size_t num_u8_char = 0;for ( std::size_t i = 0; i < u8_str.size(); ) {const auto start_point         = u8_str.data() + i;// After RFC 3629, the maximum length of each standard UTF-8 character is 4 bytes.const std::uint32_t first_byte = static_cast<std::uint32_t>( *start_point );auto integrity_checker         = [start_point, &u8_str]( std::size_t expected_len ) {assert( start_point >= u8_str.data() );if ( u8_str.size() - ( start_point - u8_str.data() ) < expected_len )throw std::invalid_argument( "incomplete UTF-8 string" );for ( std::size_t i = 1; i < expected_len; ++i )if ( ( start_point[i] & 0xC0 ) != 0x80 )throw std::invalid_argument( "broken UTF-8 character" );};if ( ( first_byte & 0x80 ) == 0 )i += 1;else if ( ( ( first_byte & 0xE0 ) == 0xC0 ) ) {integrity_checker( 2 );i += 2;} else if ( ( first_byte & 0xF0 ) == 0xE0 ) {integrity_checker( 3 );i += 3;} else if ( ( first_byte & 0xF8 ) == 0xF0 ) {integrity_checker( 4 );i += 4;} elsethrow std::invalid_argument( "not a standard UTF-8 string" );++num_u8_char;}return num_u8_char;
}int main()
{std::cout << count_u8_char( "这里一共有九个字符" ) << std::endl;
}

在开启命令行参数 -finput-charset=UTF-8 时,程序的输出恰好是 9

不过实际上我们并不会真的去关心 U8 字符串里有几个编码字符,在实际项目中更常见的是找出每个编码字符然后进行其他的字符串操作。

如果说是像使用 std::string 一样使用一个 U8 字符串的话,还是尽量避免自己手搓比较好。毕竟首先 U8 编码是一个变长编码方案,如果要实现随机读写字符势必需要一个相对复杂的解码操作;

其次 UTF 编码字符一般都是先被解码为定长的 U32(即 Unicode Code Point)字符,再进行 CRUD 操作;而将操作结果写回到编码字符串时又需要将定长 Code Point 重新编码为变长字符。这会导致一个比较经典的问题:因为每次写回的变长字符不一定都与原先的等长,所以每次更改都有可能导致底层存储字节数据的字节数组的尾部数据在反复挪移,这是一个复杂度相对较高的操作。

此时你会需要 ICU(International Components for Unicode)。

因此本文并不会去探讨如何实现一个相对完备可用的 UTF 编码字符串;但除了 UTF 编码字符串外,还有一些问题是会在使用 UTF 编码时遇到的。

例如,在终端显示中常常会出现的问题:因为不同的 Unicode 字符的复杂性不同,它们被输出到终端时被渲染出来的字体宽度是不同的;对于 ASCII 字符表内的所有可显示字符一般都占 1 个字符宽,而对于中文文本、绝大多数的 emoji 字符则占 2 个字符宽,还有极少部分的 emoji 符号和利用了零长连接字符拼接组合而成的符号字符会占 3 个字符以上的宽度。

这种情况下,支持 UTF 编码方案并没有实现完整的字符串对象那样复杂。

终端内的 Unicode 字符的渲染宽度

通常来说,终端界面的字体渲染宽度是与字符无关的,这完全是由字体渲染引擎和使用环境决定的;如果希望 100% 确定一个字符的具体渲染宽度,就没法离开本地平台的具体语言环境配置。

如果是在 Python 中,我们可以使用 unicodedata 内的 east_asian_width() 函数解决问题。

实际上,East Asian Width 也是一个决定 Unicode 字符宽度的字符属性。

但是 East Asian Width 属性过于模糊,它只给出了窄字符、中性字符、宽字符、模糊字符等基本的字符宽度;对于一些显然零长的控制字符和软连接符,文档也将其与一些长为 1 的字符一并归为中性 Neutral,这实际上并不确切。因此实际上我还是去翻了 Unicode 的 CodeCharts 文件并对照给出了所有映射表项。

但是很不幸,现在我们要脱离第三方依赖自己手搓。不过如果我们假定目标终端使用的是等宽字体,且满足我们的“刻板印象”:大多数拉丁字母和表音文字的渲染宽度占 1 字符,而 CJK 表意文字、大部分常见符号表情,和标准中大量看似鬼画符的古代语言文字的宽度为 2 字符,近年加入标准的符号表情字符宽度为 3 字符;那么问题就很好解决了。

因为有了以上的假断,所以剩下的工作就是查阅标准 CodeCharts 文件,根据文件给出的 Code Point 范围为每个字符区间映射一个 0-3 的整数。

根据之前介绍的 Unicode 规范,这里的宽度判断函数的入参是 U32 编码的字符,也即原始 Code Point 数值。

因为是 U32 编码的字符,所以其实也可以用 char32_t 作为字符类型。

constexpr std::size_t char_width( std::uint32_t codepoint ) noexcept
{if ( codepoint <= 0x20 || ( codepoint >= 0x7F && codepoint <= 0xA0 ) )return 0; // control characters,if ( codepoint == 0xAD || ( codepoint >= 0x300 && codepoint <= 0x36F ) )return 0; // combining charactersif ( ( codepoint >= 0x2000 && codepoint <= 0x200F ) || codepoint == 0x2011|| ( codepoint >= 0x2028 && codepoint <= 0x202F )|| ( codepoint >= 0x205F && codepoint <= 0x206F ) )return 0; // General Punctuationif ( codepoint >= 0xFDD0 && codepoint <= 0xFDEF )return 0; // the standard said they aren't charactersif ( codepoint >= 0xFE00 && codepoint <= 0xFE0F )return 0; // Variation Selectorsif ( codepoint >= 0xFE20 && codepoint <= 0xFE2F )return 0; // Combining Half Marksif ( codepoint == 0xFEFF )return 0; // Zero width spaceif ( ( codepoint >= 0x1FF80 && codepoint <= 0x1FFFF )|| ( codepoint >= 0x2FF80 && codepoint <= 0x2FFFF )|| ( codepoint >= 0x3FF80 && codepoint <= 0x3FFFF )|| ( codepoint >= 0xEFF80 && codepoint <= 0xEFFFF ) )return 0; // Unassignedif ( codepoint >= 0xE0000 && codepoint <= 0xE007F )return 0; // Tagsif ( codepoint >= 0xE0100 && codepoint <= 0xE01EF )return 0; // Variation Selectors Supplementif ( codepoint >= 0x21 && codepoint <= 0x7E )return 1; // ASCIIif ( codepoint >= 0xA1 && codepoint <= 0x2FF && codepoint != 0xAD )return 1; // Latin Extendedif ( ( codepoint >= 0x370 && codepoint <= 0x1FFF ) || codepoint == 0x2010|| ( codepoint >= 0x2012 && codepoint <= 0x2027 ) // These are General Punctuation|| ( codepoint >= 0x2030 && codepoint <= 0x205E )|| ( codepoint >= 0x2070 && codepoint <= 0x2E7F ) )return 1; // other languages' characters and reserved characters// I believe they are rendered to 1 character width (not pretty sure).if ( codepoint >= 0xA4D0 && codepoint <= 0xA95F )return 1; // Lisu, Vai, Cyrillic Extended and other characters with 1 widthif ( codepoint >= 0xA980 && codepoint <= 0xABFF )return 1; // Javanese, not that Java run on JVM; and other charactersif ( ( codepoint >= 0xFB00 && codepoint <= 0xFDCF ) // Alphabetic Presentation Forms|| ( codepoint >= 0xFDF0 && codepoint <= 0xFDFF ) )return 1; // Arabic Presentation Forms-Aif ( codepoint >= 0xFE70 && codepoint <= 0xFEFE )return 1; // Arabic Presentation Forms-Bif ( ( codepoint >= 0xFF61 && codepoint <= 0xFFDF )|| ( codepoint >= 0xFFE7 && codepoint <= 0xFFEF ) )return 1; // Halfwidth Formsif ( codepoint >= 0xFFF0 && codepoint <= 0xFFFF )return 1; // Specialsif ( codepoint >= 0x2E80 && codepoint <= 0xA4CF )return 2; // CJK characters, phonetic scripts and reserved characters// including many other symbol charactersif ( codepoint >= 0xA960 && codepoint <= 0xA97F )return 2; // Hangul Jamo Extendedif ( codepoint >= 0xAC00 && codepoint <= 0xD7FF )return 2; // Hangul Syllables and its extended block// U+D800 to U+DFFF is Unicode Surrogate Range,if ( codepoint >= 0xF900 && codepoint <= 0xFAD9 )return 2; // CJK Compatibility Ideographsif ( codepoint >= 0xFE10 && codepoint <= 0xFE1F )return 2; // Vertical Formsif ( codepoint >= 0xFE30 && codepoint <= 0xFE6F )return 2; // CJK Compatibility Forms and Small Form Variantsif ( ( codepoint >= 0xFF00 && codepoint <= 0xFF60 )|| ( codepoint >= 0xFFE0 && codepoint <= 0xFFE6 ) )return 2; // Fullwidth Formsif ( codepoint >= 0x10000 && codepoint <= 0x1F8FF )return 2;                                           // Some complex characters, including emojisif ( ( codepoint >= 0x20000 && codepoint <= 0x2A6DF ) // B|| ( codepoint >= 0x2A700 && codepoint <= 0x2B81D )   // C and D|| ( codepoint >= 0x2B820 && codepoint <= 0x2CEA1 )   // E|| ( codepoint >= 0x2CEB0 && codepoint <= 0x2EBE0 )   // F|| ( codepoint >= 0x2EBF0 && codepoint <= 0x2EE5D ) ) // Ireturn 2; // CJK Unified Ideographs Extension, B to Iif ( codepoint >= 0x2F800 && codepoint <= 0x2FA1D )return 2;                                           // CJK Compatibility Ideographs Supplementif ( ( codepoint >= 0x30000 && codepoint <= 0x3134A ) // G|| ( codepoint >= 0x31350 && codepoint <= 0x323AF ) ) // Hreturn 2; // CJK Unified Ideographs Extension, G to Hif ( ( codepoint >= 0xE000 && codepoint <= 0xF8FF )|| ( codepoint >= 0xFFF80 && codepoint <= 0xFFFFF )|| ( codepoint >= 0x10FF80 && codepoint <= 0x10FFFF ) )return 2; // Private Use Area and its Supplementaryif ( codepoint >= 0x1F900 && codepoint <= 0x1FBFF )return 3; // new emojisreturn 1; // Default fallback
}

其实这里还有一个 Private Use Area 也被计入了 2 字符宽度,这部分是刻意留给私人使用的。

例如苹果就在这一部分区域为每个设备实现了一个 Apple 图标的 Unicode 符号;以及一些游戏的 UI 图标都会被做成字体,然后存放在这个位置。

与此同时,为了能够将 U8 字符解码为 U32 代码点,所以我们还需要实现简单的 U8 到 U32 的解码。

总之我们能得到这样一坨东西:

std::size_t render_width( std::string_view u8_str )
{std::size_t width = 0;for ( std::size_t i = 0; i < u8_str.size(); ) {const auto start_point = u8_str.data() + i;// After RFC 3629, the maximum length of each standard UTF-8 character is 4 bytes.const auto first_byte  = static_cast<std::uint32_t>( *start_point );auto integrity_checker = [start_point, &u8_str]( std::size_t expected_len ) -> void {assert( start_point >= u8_str.data() );if ( u8_str.size() - ( start_point - u8_str.data() ) < expected_len )throw std::invalid_argument( "incomplete UTF-8 string" );for ( std::size_t i = 1; i < expected_len; ++i )if ( ( start_point[i] & 0xC0 ) != 0x80 )throw std::invalid_argument( "broken UTF-8 character" );};std::uint32_t utf_codepoint = {};if ( ( first_byte & 0x80 ) == 0 ) {utf_codepoint = static_cast<std::uint32_t>( first_byte );i += 1;} else if ( ( ( first_byte & 0xE0 ) == 0xC0 ) ) {integrity_checker( 2 );utf_codepoint = ( ( static_cast<std::uint32_t>( first_byte ) & 0x1F ) << 6 )| ( static_cast<std::uint32_t>( start_point[1] ) & 0x3F );i += 2;} else if ( ( first_byte & 0xF0 ) == 0xE0 ) {integrity_checker( 3 );utf_codepoint = ( ( static_cast<std::uint32_t>( first_byte ) & 0xF ) << 12 )| ( ( static_cast<std::uint32_t>( start_point[1] ) & 0x3F ) << 6 )| ( static_cast<std::uint32_t>( start_point[2] ) & 0x3F );i += 3;} else if ( ( first_byte & 0xF8 ) == 0xF0 ) {integrity_checker( 4 );utf_codepoint = ( ( static_cast<std::uint32_t>( first_byte ) & 0x7 ) << 18 )| ( ( static_cast<std::uint32_t>( start_point[1] ) & 0x3F ) << 12 )| ( ( static_cast<std::uint32_t>( start_point[2] ) & 0x3F ) << 6 )| ( static_cast<std::uint32_t>( start_point[3] ) & 0x3F );i += 4;} elsethrow std::invalid_argument( "not a standard UTF-8 string" );width += char_width( utf_codepoint );}return width;
}

我们可以实际测试一下:

#include <cassert>
#include <cstdint>
#include <iostream>
#include <string_view>// 包含以上两个函数int main()
{
#if _WIN32system( "chcp 65001" );
#endifstd::cout << "👨‍👩‍👧‍👦" << ": " << render_width( "👨‍👩‍👧‍👦" )<< std::endl;std::cout << "你好" << ": " << render_width( "你好" ) << std::endl;std::cout << "お幸せに" << ": " << render_width( "お幸せに" ) << std::endl;std::cout << "🥳" << ": " << render_width( "🥳" ) << std::endl;std::cout << "█" << ": " << render_width( "█" ) << std::endl;std::cout << "🇨🇳" << ": " << render_width( "🇨🇳" ) << std::endl;
}

其中 🇨🇳 和 👨‍👩‍👧‍👦 是非常典型的由若干个零宽连接字符拼接多个字符而成的单字符。

在使用 -fexec-charset=UTF-8-finput-charset=UTF-8 时,程序的输出如下。

utf-render-width

至少在终端上的字体渲染所占用的宽度和计算得到的相同。

进一步优化

实际上,函数 char_width 中硬编码的 if-else 链虽然直观,但是这一连串的条件分支对 CPU 的分支预测器极其不友好;而且很显然对于每个字符我们都需要自上而下地遍历每一个条件,其时间复杂度为 O(n)

因此我们需要利用查表优化这个宽度判断过程。

之前说过,CodeCharts 文件中的每个语言的一部分字符都具有局部集中的特点;也就是说对于具有相同字体渲染宽度的字符,它们有很大可能都集中分布在 Code Point 的某一个区间内;并且 Unicode 字符具有唯一编码的性质决定了这些区间永远不可能重叠。

那么此时查表可以简化为一个有序数组上的二分查找问题,这是一个 O(logn) 的操作。

显然每个字符区间都是已知的,所以现在只需要手工将 if-else 的编码转写为区间段并排序就行。

#include <algorithm>
#include <cassert>class CodeChart { // 引入新类型表示区间段std::uint32_t start_, end_;std::size_t width_;public:constexpr CodeChart( std::uint32_t start, std::uint32_t end, std::size_t width ) noexcept: start_ { start }, end_ { end }, width_ { width }{assert( start_ <= end_ );}~CodeChart() noexcept = default;constexpr bool contains( std::uint32_t codepoint ) const noexcept{return start_ <= codepoint && codepoint <= end_;}constexpr std::size_t width() const noexcept { return width_; }constexpr std::uint32_t size() const noexcept { return end_ - start_ + 1; }constexpr std::uint32_t head() const noexcept { return start_; }constexpr std::uint32_t tail() const noexcept { return end_; }friend constexpr bool operator<( const CodeChart& a, const CodeChart& b ) noexcept{return a.end_ < b.start_;}friend constexpr bool operator>( const CodeChart& a, const CodeChart& b ) noexcept{return a.start_ > b.end_;}friend constexpr bool operator>( const CodeChart& a, const std::uint32_t& b ) noexcept{return a.start_ > b;}friend constexpr bool operator<( const CodeChart& a, const std::uint32_t& b ) noexcept{return a.end_ < b;}
};const std::array<CodeChart, 55>& code_charts() noexcept
{// See the Unicode CodeCharts documentation for complete code points.static constexpr std::array<CodeChart, 55> chart = {{ { 0x0, 0x20, 0 },         { 0x21, 0x7E, 1 },       { 0x7F, 0xA0, 0 },{ 0xA1, 0xAC, 1 },        { 0xAD, 0xAD, 0 },       { 0xAE, 0x2FF, 1 },{ 0x300, 0x36F, 0 },      { 0x370, 0x1FFF, 1 },    { 0x2000, 0x200F, 0 },{ 0x2010, 0x2010, 1 },    { 0x2011, 0x2011, 0 },   { 0x2012, 0x2027, 1 },{ 0x2028, 0x202F, 0 },    { 0x2030, 0x205E, 1 },   { 0x205F, 0x206F, 0 },{ 0x2070, 0x2E7F, 1 },    { 0x2E80, 0xA4CF, 2 },   { 0xA4D0, 0xA95F, 1 },{ 0xA960, 0xA97F, 2 },    { 0xA980, 0xABFF, 1 },   { 0xAC00, 0xD7FF, 2 },{ 0xE000, 0xF8FF, 2 },    { 0xF900, 0xFAD9, 2 },   { 0xFB00, 0xFDCF, 1 },{ 0xFDD0, 0xFDEF, 0 },    { 0xFDF0, 0xFDFF, 1 },   { 0xFE00, 0xFE0F, 0 },{ 0xFE10, 0xFE1F, 2 },    { 0xFE20, 0xFE2F, 0 },   { 0xFE30, 0xFE6F, 2 },{ 0xFE70, 0xFEFE, 1 },    { 0xFEFF, 0xFEFF, 0 },   { 0xFF00, 0xFF60, 2 },{ 0xFF61, 0xFFDF, 1 },    { 0xFFE0, 0xFFE6, 2 },   { 0xFFE7, 0xFFEF, 1 },{ 0xFFF0, 0xFFFF, 1 },    { 0x10000, 0x1F8FF, 2 }, { 0x1F900, 0x1FBFF, 3 },{ 0x1FF80, 0x1FFFF, 0 },  { 0x20000, 0x2A6DF, 2 }, { 0x2A700, 0x2B81D, 2 },{ 0x2B820, 0x2CEA1, 2 },  { 0x2CEB0, 0x2EBE0, 2 }, { 0x2EBF0, 0x2EE5D, 2 },{ 0x2F800, 0x2FA1D, 2 },  { 0x2FF80, 0x2FFFF, 0 }, { 0x30000, 0x3134A, 2 },{ 0x31350, 0x323AF, 2 },  { 0x3FF80, 0x3FFFF, 0 }, { 0xE0000, 0xE007F, 0 },{ 0xE0100, 0xE01EF, 0 },  { 0xEFF80, 0xEFFFF, 0 }, { 0xFFF80, 0xFFFFF, 2 },{ 0x10FF80, 0x10FFFF, 2 } }};return chart;
}std::size_t char_width( std::uint32_t codepoint ) noexcept
{const auto& charts = code_charts();assert( std::is_sorted( charts.cbegin(), charts.cend() ) );// Compares with the `if-else` version, here we can search for code points with O(logn).const auto itr = std::lower_bound( charts.cbegin(), charts.cend(), codepoint );if ( itr != charts.cend() && itr->contains( codepoint ) )return itr->width();return 1; // Default fallback
}

使用起来和纯 if-else 的没区别,虽然可能内存开销会大一点点,并且映射信息也不再直观。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/501852.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python爬虫 - 豆瓣图书数据爬取、处理与存储

文章目录 前言一、使用版本二、需求分析1. 分析要爬取的内容1.1 分析要爬取的单个图书信息1.2 爬取步骤1.2.1 爬取豆瓣图书标签分类页面1.2.2 爬取分类页面1.2.3 爬取单个图书页面 1.3 内容所在的标签定位 2. 数据用途2.1 基础分析2.2 高级分析 3. 应对反爬机制的策略3.1 使用 …

MIPI_DPU 综合(DPU+MIPI+Demosaic+VDMA 通路)

目录 1. 简介 2. 创建 Platform 2.1 Block Design 2.1.1 DPU PFM Lite 2.1.2 DPU prj 2.1.3 DPU MIPI Platform 2.2 pin 约束 2.2.1 GPIO 约束 2.2.2 IIC 约束 2.1.3 DPHY 约束 3. 报错总结 3.1 AXI_M 必须顺序引用 3.2 DPU 地址分配错误 4. Design Example 4.…

Spring系列一:spring的安装与使用

文章目录 ?? 官方资料 ??Spring5下载??文档介绍 ??Spring5 ??内容介绍??重要概念 ??快速入门 ??Spring操作演示??类加载路径??Spring容器结构剖析??Debug配置 ??实现简单基于XML配置程序 ??Spring原生容器结构梳理??作业布置??Spring课堂练习 …

AutoSar架构学习笔记

1.AUTOSAR&#xff08;Automotive Open System Architecture&#xff0c;汽车开放系统架构&#xff09;是一个针对汽车行业的软件架构标准&#xff0c;旨在提升汽车电子系统的模块化、可扩展性、可重用性和互操作性。AUTOSAR的目标是为汽车电子控制单元&#xff08;ECU&#xf…

Kernel Stack栈溢出攻击及保护绕过

前言 本文介绍Linux内核的栈溢出攻击&#xff0c;和内核一些保护的绕过手法&#xff0c;通过一道内核题及其变体从浅入深一步步走进kernel世界。 QWB_2018_core 题目分析 start.sh qemu-system-x86_64 \-m 128M \-kernel ./bzImage \-initrd ./core.cpio \-append "…

【顶刊TPAMI 2025】多头编码(MHE)之Part 6:极限分类无需预处理

目录 1 标签分解方法的消融研究2 标签分解对泛化的影响3 讨论4 结论 论文&#xff1a;Multi-Head Encoding for Extreme Label Classification 作者&#xff1a;Daojun Liang, Haixia Zhang, Dongfeng Yuan and Minggao Zhang 单位&#xff1a;山东大学 代码&#xff1a;https:…

友元和运算符重载

1. 友元 可以把某些选定的函数看作类的“荣誉函数”&#xff0c;允许它们访问类对象中非公共的成员&#xff0c;就好像它们是类的成员一样&#xff0c;这种函数称为类的友元。友元可以访问类对象的任意成员。 1.1 友元函数 友元函数是一种定义在类外部的普通函数&#xff0…

Git 树形图表不显示问题

注册表修改 ## 注册表 计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdentifiers 生效 右键重启 windows资源管理器

【MySQL基础篇】三、表结构的操作

文章目录 Ⅰ. 创建表1、语法2、创建表样例3、创建和其它表一样结构的表 Ⅱ. 查看表结构1、查看数据库中的表2、查看指定表的属性3、获取表的创建语句 Ⅲ. 删除表Ⅳ. 修改表结构1、向表中插入新的字段2、删除表中的字段3、修改表名4、修改字段属性 Ⅰ. 创建表 1、语法 create …

aws(学习笔记第二十二课) 复杂的lambda应用程序(python zip打包)

aws(学习笔记第二十二课) 开发复杂的lambda应用程序(python的zip包) 学习内容&#xff1a; 练习使用CloudShell开发复杂lambda应用程序(python) 1. 练习使用CloudShell CloudShell使用背景 复杂的python的lambda程序会有许多依赖的包&#xff0c;如果不提前准备好这些python的…

SAP SD销售模块常见BAPI函数

【SAP系统研究】 #SAP #SD #销售管理 1、销售订单 BAPI_SALESORDER_CREATEFROMDAT2 创建销售订单 BAPI_CUSTOMERRETURN_CREATE 创建退货订单 SD_SALESDOCUMENT_CREATE 创建贷项订单 BAPI_SALESORDER_CHANGE 修改销售订单 STATUS_READ 查看销售订单状态VB销售订单000000 I_CHA…

CSS学习记录21

CSS 工具提示 通过CSS 创建工具提示&#xff08;Tooltip)。 当用户将鼠标指针移动到元素上时&#xff0c;工具提示通常用于提供关于某内容的额外信息&#xff1a; <style> /* Tooltip 容器 */ .tooltip {position: relative;display: inline-block;border-bottom: 1px …

DuckDB:密钥管理器及其应用

密钥管理器(Secrets Manager)为所有使用密钥的后端提供了统一的用户界面。密钥信息可以被限定范围&#xff0c;因此不同的存储前缀可以有不同的密钥信息&#xff0c;例如允许在单个查询中连接跨组织的数据。密钥也可以持久化&#xff0c;这样就不需要在每次启动DuckDB时都指定它…

[cg] android studio 无法调试cpp问题

折腾了好久&#xff0c;native cpp库无法调试问题&#xff0c;原因 下面的Deploy 需要选Apk from app bundle!! 另外就是指定Debug type为Dual&#xff0c;并在Symbol Directories 指定native cpp的so路径 UE项目调试&#xff1a; 使用Android Studio调试虚幻引擎Android项目…

uni-app深度解码:跨平台APP开发的核心引擎与创新实践

在当今数字化浪潮中&#xff0c;移动应用市场呈现出爆炸式增长。为了满足不同用户群体在不同操作系统上的需求&#xff0c;跨平台 APP 开发成为众多开发者的首选策略。uni-app 作为一款领先的跨平台开发框架&#xff0c;以其独特的优势和创新的实践在众多同类产品中脱颖而出。它…

I2C(一):存储器模式:stm32作为主机对AT24C02写读数据

存储器模式&#xff1a;在HAL库中&#xff0c;I2C有专门对存储器外设设置的库函数 I2C&#xff08;一&#xff09;&#xff1a;存储器模式的使用 1、I2C轮询式写读AT24C02一页数据2、I2C轮询式写读AT24C02多页数据3、I2C中断式写读AT24C02一页数据4、I2C使用DMA式写读AT24C02一…

Elasticsearch:减少 Elastic 容器镜像中的 CVE(常见的漏洞和暴露)

作者&#xff1a;来自 Elastic Maxime Greau 在这篇博文中&#xff0c;我们将讨论如何通过在 Elastic 产品中切换到最小基础镜像并优化可扩展漏洞管理程序的工作流程来显著减少 Elastic 容器镜像中的常见漏洞和暴露 (Common Vulnerabilities and Exposures - CVEs)。 基于 Chai…

【AI学习】Transformer深入学习(二):从MHA、MQA、GQA到MLA

前面文章&#xff1a; 《Transformer深入学习&#xff08;一&#xff09;&#xff1a;Sinusoidal位置编码的精妙》 一、MHA、MQA、GQA 为了降低KV cache&#xff0c;MQA、GQA作为MHA的变体&#xff0c;很容易理解。 多头注意力&#xff08;MHA&#xff09;&#xff1a; 多头注…

使用python调用翻译大模型实现本地翻译【exe客户端版】

以前分享过一个 关于python 部署 网页端的 翻译大模型的 文章 有兴趣的小伙伴可以去看一下 https://blog.csdn.net/Drug_/article/details/144488795 今天就再分享一个 使用python 来制作一个 exe 客户端版的 本地大模型。 实际也很简单 只不过把 用 fastApi 框架 做的 网页端…

python3GUI--智慧交通监控与管理系统 By:PyQt5

文章目录 一&#xff0e;前言二&#xff0e;预览三&#xff0e;软件组成&技术难点1.软件组成结构2.技术难点3.项目结构 四&#xff0e;总结 大小&#xff1a;35.5 M&#xff0c;软件安装包放在了这里! 一&#xff0e;前言 博主高产&#xff0c;本次给大家带来一款我自己使…