Skip to content

char8_t 与 UTF-8 字符串

在 C++20 之前,UTF-8 字符串字面量 u8"..." 的类型是 const char[N]——跟普通字符串在类型上压根没区别。这听着好像无所谓,其实是不少坑的老巢:你没法在类型层面分清"这一串是 UTF-8"还是"这一串是本地执行字符集",编译器也帮不上你挡住那种把 UTF-8 当裸字节乱打印的错。C++20 引入 char8_t,就是要把 UTF-8 从 char 那片模糊地带里独立出来,给它一个专属类型,让类型系统替咱们把关。这改动来自提案 P0482R6「char8_t: A type for UTF-8 characters and strings」,探测支持看 __cpp_char8_t(C++20,值 201811L)。

不过——笔者得提前打个预防针——这个"独立类型"的改动是带破坏性的:它一把改了 u8 字面量的类型,于是一大批在 C++17 下岁月静好的老代码,升到 C++20 直接就编不过了。这一篇,我们就把这俩最常踩的坑、怎么搬代码、还有 C++23 后来补的那一刀,一次讲清楚。


u8 字面量,类型整个换了灵魂

C++20 起,UTF-8 字符串字面量 u8"..." 的类型,从 const char[N] 变成了 const char8_t[N];UTF-8 字符字面量 u8'c' 的类型,也从 char 变成了 char8_t。这个 char8_t 是个独立的 fundamental 类型,底层类型(underlying type)是 unsigned char,大小、对齐、转换秩都跟 unsigned char 一致——但它不参与别名规则(不是 [basic.lval] 里允许别名访问的那几个类型之一),也就是说,你不能拿 char8_t* 去合法地别名访问别的对象内存。

至于为啥要这么较真地单造一个类型?道理很简单:类型一旦分开,编译器就能在"把 UTF-8 串误当本地编码 char 串使""把 char8_t 当整数打印"这类错误上直接报错,而不是等运行时输出一屏乱码才让你拍大腿。拿类型安全换一点点迁移成本,这笔账 C++20 觉得值。

两个最经典的坑

类型一换,两个迁移坑就浮上水面了。

头一个坑:u8"" 不能再隐式转 const char* C++17 里,const char* p = u8"text"; 完全合法(那会儿 charchar8_t 还是一家人);到了 C++20,u8"text" 成了 const char8_t[N],而 char8_t 不会隐式转 char,这行直接 ill-formed。所有把 u8 字面量塞给期望 const char* 的旧接口(构造 std::string、传给 C API、std::filesystem::u8path 的某些重载等等)统统中招。

第二个坑:标准库故意 =deletechar8_t 的 ostream 重载。 您可能想——那我直接 std::cout << u8"text"; 打呗?也不行。C++20 起,标准库对 char8_tconst char8_t* 这类 UTF-8 字符/字符串,在 basic_ostream<char>basic_ostream<wchar_t> 上的 operator<< 重载,是显式删除的(注意,不是"忘了实现",是故意的)。于是 std::cout << u8'z'std::cout << u8"text" 都会因为命中 deleted 重载而编译失败。这么干,就是为了拦住历史代码把 UTF-8 数据当整数或指针胡乱打印出来。

老代码怎么搬过来

碰上这俩坑,怎么把 C++17 的老代码挪到 C++20?几条路,笔者按代价从低到高给您捋:

最省事的,是编译选项回退:GCC/Clang 上加 -fno-char8_t、MSVC 上加 /Zc:char8_t-,把 u8 字面量的类型退回 C++17 的 char 语义,老代码立马又能编。这只是过渡期的权宜之计,新代码别长期依赖它。其次,是显式逐字节转换:当你确实要喂给一个只认 const char* 的接口、且心里有数内容就是 UTF-8 字节时,用 reinterpret_cast<const char*>(u8"text")(或者 C 风格转换)切个视角——字节内容不变,只是换个指针类型,把"头一个坑"绕过去。最"政治正确"的,是std::u8string 路线:用 u8string/u8string_view 类型安全地持有 UTF-8 文本,要打印时再写个小小的 operator<< 把它转出去,把类型安全贯彻到底。

C++23 的 P2513:又补回来一点

"头一个坑"里"不能初始化"的范围,后来倒是被收窄了一点点。提案 P2513R4「char8_t Compatibility and Portability」作为 C++20 的缺陷报告(DR),在 C++23 落地(__cpp_char8_t 的值也跟着改成 202207L),重新允许用 u8 字符串字面量去初始化 charunsigned char 数组——也就是 char ca[] = u8"text"; 这种又变回合法了。但请注意,它只放宽了"数组初始化"这一条;const char8_t*const char* 的指针隐式转换,至今仍然 ill-formed,坑一里那个指针赋值的情形,可没被放过。


上手跑一跑

下面这个 demo,把两个坑(笔者用注释"封印"起来,您取消注释立马编译失败)和两种正确写法摆一块儿,方便对照。

展开代码 (共 33 行)收起代码
cpp
// Standard: C++20  | Platform: host
#include <iostream>
#include <string>

// —— 坑一(取消注释会编译失败):u8"" 不再隐式转 const char* ——
// const char* p = u8"text";   // ill-formed since C++20

// —— 坑二(取消注释会编译失败):ostream 显式 =delete 了 char8_t 重载 ——
// std::cout << u8"text";      // ill-formed since C++20
// std::cout << u8'z';         // ill-formed since C++20

// 正确写法之一:显式逐字节转换(内容不变,仅切换指针类型视角)
void print_as_char(const char* s)
{
    std::cout << s << '\n';
}

// 正确写法之二:用 std::u8string 类型安全地持有 UTF-8,并自定义打印
std::ostream& operator<<(std::ostream& os, const std::u8string& s)
{
    return os << reinterpret_cast<const char*>(s.data());
}

int main()
{
    // 路线 A:把 u8 字面量当 const char* 用(适合喂给只认窄字符的旧接口)
    print_as_char(reinterpret_cast<const char*>(u8"text"));

    // 路线 B:u8string 全程保持 UTF-8 类型,打印时再转
    std::u8string u8s = u8"UTF-8 text";
    std::cout << u8s << '\n';
    return 0;
}

Compiler Explorer

char8_t 与 UTF-8 字符串:两坑与正确写法

演示 C++20 u8 字面量类型变更的两个编译失败坑,以及显式转换与 u8string 两种正确写法

code/examples/vol3/14_char8_t.cpp

参考资源

v0.7.0-9-g940ec1b · 940ec1b · 2026-07-05