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 232−1 个字符,同时它的编码数字增长也不连续。
举例来说,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::u16string
和 std::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
时,程序的输出如下。
至少在终端上的字体渲染所占用的宽度和计算得到的相同。
进一步优化
实际上,函数 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
的没区别,虽然可能内存开销会大一点点,并且映射信息也不再直观。
相关文章:

C++ 中 Unicode 字符串的宽度
首先,什么是 Unicode? Unicode 实际上是一个统一的文字编码标准,它出现目的是为了解决不同计算机之间字符编码不同而导致的灾难性不兼容问题。 Unicode 字符集与 Unicode 编码是两种不同的概念。Unicode 字符集实际是对进入标准的所有文字用…...

人工智能在SEO中的应用与关键词优化策略
内容概要 随着科技的迅猛发展,人工智能在搜索引擎优化(SEO)中的应用逐渐成为业界关注的热点。AI技术不仅可以有效提高关键词的优化策略,还能在提升内容效率、增强用户体验方面发挥重要作用。通过对相关技术的深入探讨,…...
spring mvc源码学习笔记之四
pom.xml 内容如下 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/P…...

ruckus R510升级到Unleashe后不能访问
ruckus R510 是IPQ4019,升级到Unleashe,它弹窗提示 但是这个IP没办法用,访问不了AP。 必应了一下,官方提示用advance ip scanner扫描。 扫描持续好久,发现IP竟然是从主路由获得。 9090的端口不用填,甚至不…...

【游戏设计原理】47 - 超游戏思维
对于这条原理,我首先想到的是开放世界,或者探索性游戏,这是最能包容各类玩家的游戏类型。这类游戏定义了基本规则,玩家的可操作性很强。就像上图里的沙池一样,里面有滑梯,是规则性比较明确的,而…...
FastAPI vs Flask 专业对比与选择
FastAPI与Flask是两个流行的Python Web框架,它们在构建Web应用程序和API方面各有特点。以下是对这两个框架的详细比较: 一、设计理念与用途 Flask: 是一个轻量级的Python Web框架,基于Werkzeug WSGI工具箱和Jinja2模板引擎。设计…...

【信息系统项目管理师】【综合知识】【备考知识点】【思维导图】第十一章 项目成本管理
word版☞【信息系统项目管理师】【综合知识】【备考知识点】第十一章 项目成本管理 移动端【思维导图】☞【信息系统项目管理师】【思维导图】第十一章 项目成本管理...
xdoj-字符串-556,为什么字符不能被正常读入
目录 题目 代码 测试用例 the input the correct output 问题发现过程阐述 如果把line16中的数组大小11换成line17中的10 case 1 case 2 case 3 如果数组开成11 case4 代码分析 问题描述 Question1 Question2 题目 题目:连续数字字符串提取 问题描述…...

计算机网络——期末复习(5)期末考试样例1(含答案)
考试题型; 概念辨析5个、计算与分析3个、综合题3-4个 必考知识点: 概述:协议 体系结构 物理层;本次考核较少 链路层:CSMA/CD 退避二进制算法 ࿰…...

Docker安装oracle数据库【最新版】
文章目录 1. 安装 Docker 环境2. 拉取 Oracle 镜像3. 查看镜像4. 创建容器5. 进入容器进行配置6. 进行软连接7. 配置 Oracle 环境变量8. 创建软连接9. 切换到 Oracle 用户10. 登录 SQL*Plus 并修改 sys、system 用户密码11. 重新启动数据库12. 解决 "Database Not Open&qu…...

基于STM32的智能门锁系统设计
目录 引言系统设计 硬件设计软件设计系统功能模块 用户身份验证模块开锁控制模块状态监控与报警模块数据存储与管理模块控制算法 用户身份验证算法开锁控制算法状态监控与报警算法代码实现 用户身份验证模块实现开锁控制模块实现状态监控模块实现系统调试与优化结论与展望 1. …...
【踩坑指南:2025年最新】如何在Linux(Ubuntu)启动第一个Scala Hello World程序(Scala3)
如何正确地写出Scala的第一个程序,并且利用Scala3的简洁特性? 在解释器中直接输出Hello world非常简单,只需要直接执行即可: scala> println("Hello World") Hello World 但如果我们希望编写一个脚本文件…...

SAP系统中的标准价、移动平均价是什么?有何区别?物料分类账的优点
文章目录 前言一、SAP系统中的价格控制二、移动平均价、标准价是什么?三、S价(标准价)的优势四、S价(标准价)的劣势五、V价(移动平均价)的优势六、V价(移动平均价)的劣势…...

9.类的定义与使用
类的定义构造函数(__init__)实例变量类变量方法(实例方法)类方法(classmethod)静态方法(staticmethod)属性装饰器(property)私有属性与方法继承多态方法重写super()函数类的文档字符串类的属性和方法访问控制 1.类的定义: 如int,list,tuple等等都是类,还可以通过class方法自己…...

【网络安全 | 漏洞挖掘】JS Review + GraphQL滥用实现管理面板访问
未经许可,不得转载。 正文 在映射目标范围后,我发现了一个用于管理的控制台界面,但没有注册功能。 于是我开始尝试: 1、模糊测试注册端点 -> 失败 2、在请求中将登录替换为注册 -> 再次失败 尝试均未奏效后,我决定冷静下来,重新思考方法并利用技术手段。 我观察…...

前端如何判断多个请求完毕
在前端开发中,经常会遇到需要同时发起多个异步请求,并在所有请求都完成后再进行下一步操作的情况。 这里有几个常用的方法来实现这一需求: 使用 Promise.all() Promise.all() 方法接收一个 Promise 对象的数组作为参数,当所有的…...
atrust异常导致ERR_NETWORK_CHANGED
首先因为工作需要不断安装卸载不同版本深信服的atrust。那么可能遇到和我一样的问题。 深信服的这种东西有点毛病,以前只是偶尔导致我局域网无法访问,我停止atrust后,他还有后台程序在后台不断更改我的适配器,在我局域网需要固定…...
【网络安全 | 漏洞挖掘】绕过电子邮件确认实现预账户接管
未经许可,不得转载。 文章目录 正文漏洞步骤赏金正文 我测试的应用程序有多个子域名: 1、account.example.com:处理用户账户管理。 2、project.example.com:管理用户拥有或被邀请的项目。 3、org.example.com:一个新的子域,用于管理多个项目的组织。 4、collaborator.ex…...

python3GUI--智慧交通监控与管理系统 By:PyQt5
文章目录 一.前言二.预览三.软件组成&技术难点1.软件组成结构2.技术难点3.项目结构 四.总结 大小:35.5 M,软件安装包放在了这里! 一.前言 博主高产,本次给大家带来一款我自己使…...
Java重要面试名词整理(十八):Sentinel
文章目录 服务雪崩及其解决方案技术选型: Sentinel or Hystrix 流量治理组件Sentinel**基于SentinelResource注解埋点实现资源保护** Sentinel控制台**限流阈值类型****流控模式****直接****关联** **流控效果**系统规则——系统自适应保护 限流算法**计数器法****滑动时间窗口…...
系统设计 --- MongoDB亿级数据查询优化策略
系统设计 --- MongoDB亿级数据查询分表策略 背景Solution --- 分表 背景 使用audit log实现Audi Trail功能 Audit Trail范围: 六个月数据量: 每秒5-7条audi log,共计7千万 – 1亿条数据需要实现全文检索按照时间倒序因为license问题,不能使用ELK只能使用…...
Java - Mysql数据类型对应
Mysql数据类型java数据类型备注整型INT/INTEGERint / java.lang.Integer–BIGINTlong/java.lang.Long–––浮点型FLOATfloat/java.lang.FloatDOUBLEdouble/java.lang.Double–DECIMAL/NUMERICjava.math.BigDecimal字符串型CHARjava.lang.String固定长度字符串VARCHARjava.lang…...

04-初识css
一、css样式引入 1.1.内部样式 <div style"width: 100px;"></div>1.2.外部样式 1.2.1.外部样式1 <style>.aa {width: 100px;} </style> <div class"aa"></div>1.2.2.外部样式2 <!-- rel内表面引入的是style样…...
Java入门学习详细版(一)
大家好,Java 学习是一个系统学习的过程,核心原则就是“理论 实践 坚持”,并且需循序渐进,不可过于着急,本篇文章推出的这份详细入门学习资料将带大家从零基础开始,逐步掌握 Java 的核心概念和编程技能。 …...
OpenLayers 分屏对比(地图联动)
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 地图分屏对比在WebGIS开发中是很常见的功能,和卷帘图层不一样的是,分屏对比是在各个地图中添加相同或者不同的图层进行对比查看。…...

【JavaWeb】Docker项目部署
引言 之前学习了Linux操作系统的常见命令,在Linux上安装软件,以及如何在Linux上部署一个单体项目,大多数同学都会有相同的感受,那就是麻烦。 核心体现在三点: 命令太多了,记不住 软件安装包名字复杂&…...

tree 树组件大数据卡顿问题优化
问题背景 项目中有用到树组件用来做文件目录,但是由于这个树组件的节点越来越多,导致页面在滚动这个树组件的时候浏览器就很容易卡死。这种问题基本上都是因为dom节点太多,导致的浏览器卡顿,这里很明显就需要用到虚拟列表的技术&…...

使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台
🎯 使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台 📌 项目背景 随着大语言模型(LLM)的广泛应用,开发者常面临多个挑战: 各大模型(OpenAI、Claude、Gemini、Ollama)接口风格不统一;缺乏一个统一平台进行模型调用与测试;本地模型 Ollama 的集成与前…...
【Go语言基础【13】】函数、闭包、方法
文章目录 零、概述一、函数基础1、函数基础概念2、参数传递机制3、返回值特性3.1. 多返回值3.2. 命名返回值3.3. 错误处理 二、函数类型与高阶函数1. 函数类型定义2. 高阶函数(函数作为参数、返回值) 三、匿名函数与闭包1. 匿名函数(Lambda函…...

使用Spring AI和MCP协议构建图片搜索服务
目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式(本地调用) SSE模式(远程调用) 4. 注册工具提…...