ядро языка — типыcore language — types
Отдельный тип для 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.
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.
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
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
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.
Было невозможно написать отдельные перегрузки 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.
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.
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
Обычный текст идёт как есть, заведомо 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");
Стандартный паттерн взаимодействия с 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);
Поле имени файла в кроссплатформенном коде — тип в сигнатуре сразу документирует, что байты гарантированно валидный 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};
В 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"));
Много реального кода сломалось при обновлении на 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 на этапе компиляции или рантайма сам по себе — компилятор не проверяет, что байты действительно образуют корректную 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.
Взаимодействие с 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.