首先,什么是 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
的没区别,虽然可能内存开销会大一点点,并且映射信息也不再直观。