ядро языка — типыcore language — types

char8_t

Отдельный тип для UTF-8 code unit — литерал u8"text" теперь имеет тип const char8_t*, а не const char*, разводя "байт памяти" и "заведомо UTF-8 текст" в системе типов.A dedicated type for a UTF-8 code unit — the u8"text" literal now has type const char8_t* instead of const char*, separating "memory byte" from "known-UTF-8 text" in the type system.

breaking change для существующего кодаa breaking change for existing code многие проекты сознательно не включают литералы u8many projects deliberately avoid u8 literals

Что этоWhat it is

char8_t — отдельный фундаментальный тип для представления одного UTF-8 code unit (одного байта UTF-8 последовательности), отличный от char. Строковые литералы u8"text" начиная с C++20 имеют тип const char8_t* — в C++17 и раньше тот же литерал имел тип const char*. Тип не меняет представление в памяти (это всё ещё один байт), он меняет то, что о содержимом байта знает система типов компилятора.char8_t is a distinct fundamental type representing a single UTF-8 code unit (one byte of a UTF-8 sequence), separate from char. Starting with C++20, u8"text" string literals have type const char8_t* — in C++17 and earlier, the same literal had type const char*. The type doesn't change the in-memory representation (it's still one byte); it changes what the compiler's type system knows about the byte's contents.

до C++20 — u8-литерал это char*before C++20 — a u8 literal is a char*
const char* s1 = "plain string";         // платформенная кодировка// platform encoding
const char* s2 = u8"utf-8 string";       // тот же самый тип const char*!// the very same type const char*!

void f(const char*);   // одна перегрузка — нельзя различить// one overload — can't be distinguished
// f(s1) и f(s2) неотличимы для компилятора по типу// f(s1) and f(s2) are indistinguishable to the compiler by type
C++20 — u8-литерал это char8_t*C++20 — a u8 literal is a char8_t*
const char* s1 = "plain string";         // const char*// const char*
const char8_t* s2 = u8"utf-8 string";       // const char8_t* — другой тип// const char8_t* — a different type

void f(const char*);
void f(const char8_t*);   // теперь возможна отдельная перегрузка// now a separate overload is possible

Зачем ввелиWhy it was introduced

01

char исторически совмещал три разные ролиchar historically served three different roles at once

char одновременно использовался как "маленькое целое число" (арифметика, побитовые операции), как "символ в некоторой платформенной кодировке" (обычно ASCII-совместимой) и, с введением UTF-8 литералов в C++11, как "один байт UTF-8-последовательности" — три семантически разных назначения, разделяющих один и тот же тип.char was simultaneously used as "a small integer" (arithmetic, bitwise ops), as "a character in some platform encoding" (usually ASCII-compatible), and, once UTF-8 literals appeared in C++11, as "one byte of a UTF-8 sequence" — three semantically different purposes sharing a single type.

02

Смешение ролей делало невозможной перегрузку по кодировкеThe role conflation made overloading by encoding impossible

Было невозможно написать отдельные перегрузки f(const char*) для "обычной строки в локальной кодировке" и f(const char8_t*) (тогда ещё не существовавший тип) для "заведомо UTF-8 строки", потому что оба литерала — "text" и u8"text" — имели один и тот же тип const char*. Компилятор физически не мог различить намерение программиста по одному только типу параметра.It was impossible to write separate overloads — f(const char*) for "an ordinary string in the local encoding" and f(const char8_t*) (a type that didn't yet exist) for "a known-UTF-8 string" — because both literals, "text" and u8"text", had the exact same type, const char*. The compiler had no way to distinguish the programmer's intent from the parameter type alone.

03

Тип литерала теперь однозначно указывает на предполагаемую кодировкуThe literal's type now unambiguously signals the intended encoding

char8_t решает именно эту проблему: теперь по одному типу видно, что перед нами заведомо UTF-8-данные, а не произвольный char* неизвестной кодировки. Это даёт компилятору и разработчику API возможность строить систему типов, отражающую реальные гарантии о кодировке строки, а не полагаться на соглашения и комментарии.char8_t solves exactly this: the type alone now shows that the data is known to be UTF-8, rather than an arbitrary char* of unknown encoding. This lets the compiler and API designers build a type system that reflects real guarantees about a string's encoding, rather than relying on conventions and comments.

Как это работает под капотомHow it works under the hood

char8_t — отдельный фундаментальный тип (не typedef и не alias для char или unsigned char), с теми же размером (1 байт) и представлением, что и unsigned char, но отдельный для целей перегрузки, шаблонов и std::is_same. Это тот же подход, что уже применялся к char16_t/char32_t в C++11 для UTF-16/UTF-32 — char8_t закрывает симметричный пробел для UTF-8, который до этого обходился обычным char по историческим причинам (UTF-8 литералы появились в C++11, но получили собственный тип только в C++20).char8_t is a distinct fundamental type (not a typedef or alias for char or unsigned char), with the same size (1 byte) and representation as unsigned char, but distinct for the purposes of overloading, templates, and std::is_same. This mirrors the approach already taken for char16_t/char32_t in C++11 for UTF-16/UTF-32 — char8_t fills the symmetric gap for UTF-8, which until then made do with plain char for historical reasons (UTF-8 literals appeared in C++11 but only got their own type in C++20).

static_assert(!std::is_same_v<char8_t, char>);        // разные типы// different types
static_assert(sizeof(char8_t) == 1);                 // тот же размер// same size

const char8_t* utf8 = u8"hello";
std::size_t len = std::char_traits<char8_t>::length(utf8);

// взаимодействие со старым API, ожидающим const char*:// interop with older APIs expecting const char*:
void legacy_api(const char* s);
legacy_api(reinterpret_cast<const char*>(utf8));   // явный cast обязателен// an explicit cast is mandatory

Ключевой сценарий, невозможный до char8_t, — раздельные перегрузки одной функции для "строки в неизвестной/локальной кодировке" и для "заведомо UTF-8 строки". До C++20 оба вызова разрешались бы в одну и ту же перегрузку const char*, потому что оба литерала имели один и тот же тип:The key scenario impossible before char8_t is a pair of separate overloads of the same function — one for "a string in an unknown/local encoding" and one for "a known-UTF-8 string." Before C++20 both calls would resolve to the same const char* overload, because both literals had the exact same type:

void log_message(const char* s) {
    std::fputs(s, stderr);   // кодировка неизвестна — просто байты как есть// encoding unknown — bytes passed through as-is
}
void log_message(const char8_t* s) {
    log_utf8_to_console(s);   // заведомо UTF-8 — можно применить UTF-8-специфичную обработку// known UTF-8 — safe to apply UTF-8-specific handling
}

log_message("plain text");      // выбирает перегрузку const char*// picks the const char* overload
log_message(u8"hello");       // выбирает перегрузку const char8_t*// picks the const char8_t* overload

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

Перегрузка функции логирования отдельно для char* и char8_t*A logging function overloaded separately for char* and char8_t*

Обычный текст идёт как есть, заведомо UTF-8 текст может дополнительно проверяться или конвертироваться под нужды консоли/файла.Ordinary text passes through as-is, known-UTF-8 text can additionally be validated or converted for the console's or file's needs.

void log(const char* s)   { std::fputs(s, stdout); }
void log(const char8_t* s) { log(reinterpret_cast<const char*>(s)); }

log("plain text");
log(u8"utf-8 text");
02

Конвертация char8_t-строки в char* через reinterpret_cast для legacy APIConverting a char8_t string to char* via reinterpret_cast for a legacy API

Стандартный паттерн взаимодействия с API, которые ещё не обновлены под char8_t — cast безопасен, поскольку представление байтов идентично char.The standard pattern for interop with APIs that haven't been updated for char8_t — the cast is safe since the byte representation is identical to char.

void write_to_file(const char* data, std::size_t len);

const char8_t* payload = u8"file contents";
std::size_t len = std::char_traits<char8_t>::length(payload);
write_to_file(reinterpret_cast<const char*>(payload), len);
03

char8_t в структуре, хранящей заведомо UTF-8 данныеchar8_t in a struct holding known-UTF-8 data

Поле имени файла в кроссплатформенном коде — тип в сигнатуре сразу документирует, что байты гарантированно валидный UTF-8, а не произвольная локальная кодировка.A filename field in cross-platform code — the type in the signature immediately documents that the bytes are guaranteed valid UTF-8, not some arbitrary local encoding.

struct FileEntry {
    std::u8string name;    // заведомо UTF-8, не платформенная кодировка// known UTF-8, not the platform encoding
    std::uintmax_t size_bytes;
};

FileEntry e{.name = u8"report.txt", .size_bytes = 4096};
04

Типичная ошибка компиляции: u8-литерал в функцию, ожидающую const char*A typical compile error: a u8 literal into a function expecting const char*

В C++17 это компилировалось без единого предупреждения, в C++20 — не компилируется без явного приведения типа, потому что тип литерала изменился.In C++17 this compiled without a single warning; in C++20 it doesn't compile without an explicit cast, because the literal's type changed.

void accept_c_string(const char* s);

// C++17: ок, u8"..." имел тип const char*// C++17: fine, u8"..." had type const char*
// C++20: ошибка компиляции — нет неявного char8_t* -> char* преобразования// C++20: compile error — no implicit char8_t* -> char* conversion exists
// accept_c_string(u8"hello");

// исправление — явный cast:// the fix — an explicit cast:
accept_c_string(reinterpret_cast<const char*>(u8"hello"));

ГраблиGotchas

Переход на char8_t — breaking change для реального кодаThe switch to char8_t is a breaking change for real code

Много реального кода сломалось при обновлении на C++20 именно из-за этого изменения: код, который ожидал, что u8"text" имеет тип const char* (присваивание в const char*, передача в функции, принимающие const char*), перестаёт компилироваться без изменений — это не гипотетический риск, а задокументированная практическая проблема миграции 2020–2023 годов.A lot of real code broke when upgrading to C++20 specifically because of this change: code that expected u8"text" to have type const char* (assigning it to a const char*, passing it to functions taking const char*) stops compiling unmodified — this isn't a hypothetical risk but a documented, practical migration pain point from 2020-2023.

char8_t не проверяет корректность UTF-8 — это только тип, не валидацияchar8_t doesn't validate UTF-8 — it's only a type, not a check

char8_t не даёт никакой валидации UTF-8 на этапе компиляции или рантайма сам по себе — компилятор не проверяет, что байты действительно образуют корректную UTF-8 последовательность, ни для литералов, ни тем более для данных, полученных в рантайме. Это просто более точный тип для "предполагаемого UTF-8", а не гарантия корректности кодировки.char8_t provides no UTF-8 validation at compile time or runtime by itself — the compiler doesn't check that the bytes actually form a valid UTF-8 sequence, neither for literals nor especially for data obtained at runtime. It's simply a more precise type for "presumed UTF-8," not a guarantee of encoding correctness.

Interop-боль привела к тому, что многие проекты сознательно избегают литералов u8Interop pain led many projects to deliberately avoid u8 literals

Взаимодействие с API, ожидающими const char* (большинство существующих библиотек, включая часть стандартной библиотеки до полного проброса char8_t всюду), требует явного reinterpret_cast или отдельных перегрузок — на практике многие проекты в 2020–2023 годах предпочитали НЕ включать литералы u8 в свою кодовую базу, несмотря на концептуальную правильность фичи, именно из-за этой практической боли на стыке с legacy-кодом.Interop with APIs expecting const char* (most existing libraries, including parts of the standard library before char8_t was fully threaded through) requires an explicit reinterpret_cast or separate overloads — in practice, many projects in 2020-2023 chose NOT to adopt u8 literals in their codebase, despite the feature's conceptual correctness, precisely because of this practical friction at the boundary with legacy code.