Как правильно использовать std :: string в UTF-8 в C++?

Моя платформа - Mac и C++ 11 (или выше). Я новичок C++ и работаю над личным проектом, который обрабатывает китайский и английский языки. UTF-8 является предпочтительным кодированием для этого проекта.

Я прочитал несколько сообщений о переполнении стека, и многие из них предлагают использовать std::string при работе с UTF-8 и избегать wchar_t как теперь нет char8_t для UTF-8.

Однако ни один из них не говорит о том, как правильно обращаться с такими функциями, как str[i], std::string::size(), std::string::find_first_of() или std::regex поскольку эти функции обычно возвращают неожиданные результаты при столкновении с UTF-8.

Должен ли я продолжать std::string или переключиться на std::wstring? Если я должен остаться с std::string, то для чего лучше всего справиться с вышеуказанными проблемами?

Ответ 1

Глоссарий Unicode

Unicode - это обширная и сложная тема. Я не хочу пробираться слишком глубоко туда, однако необходим быстрый глоссарий:

  1. Кодовые точки: кодовые точки являются основными строительными блоками Юникода, кодовая точка представляет собой целое число, отображаемое в значении. Целочисленная часть вписывается в 32 бита (ну, на самом деле 24 бита), а смысл может быть буквой, диакритикой, пробелом, знаком, смайликом, наполовину флагом... и даже может быть " следующая часть читается справа налево ".
  2. Кластеры Grapheme: Grapheme Clusters - это группы семантически связанных кодовых точек, например, флаг в unicode представлен объединением двух кодовых точек; каждый из этих двух, по отдельности, не имеет никакого значения, но связан вместе в кластере Графем, они представляют собой флаг. Grapheme Clusters также используются для сопряжения буквы с диакритикой в некоторых сценариях.

Это основная часть Unicode. Различие между Code Point и Grapheme Cluster может быть в основном затушевано, потому что для большинства современных языков каждый "символ" отображается в единую кодовую точку (имеются специальные акцентированные формы для обычно используемых буквенных + диакритических комбинаций). Тем не менее, если вы рискуете смайлами, флагами и т.д.... тогда вам, возможно, придется обратить внимание на различие.


UTF Primer

Затем необходимо закодировать серию кодовых точек Юникода; общие кодировки - UTF-8, UTF-16 и UTF-32, последние два из которых существуют в форматах Little-Endian и Big-Endian, в общей сложности 5 общих кодировок.

В UTF-X X - это размер в битах блока кода, каждая точка кода представляется как один или несколько блоков кода, в зависимости от его величины:

  • UTF-8: от 1 до 4 единиц кода,
  • UTF-16: 1 или 2 единицы кода,
  • UTF-32: 1 блок кода.

std::string и std::wstring.

  1. Не используйте std::wstring если вы заботитесь о переносимости (wchar_t - только 16 бит в Windows); std::u32string этого используйте std::u32string (aka std::basic_string<char32_t>).
  2. Представление in-memory (std::string или std::wstring) не зависит от представления на диске (UTF-8, UTF-16 или UTF-32), поэтому подготовьтесь к необходимости конвертировать на границе (чтение и письмо).
  3. В то время как 32-бит wchar_t гарантирует, что блок кода представляет собой полную точку кода, он по-прежнему не представляет собой полный кластер Grapheme.

Если вы только читаете или составляете строки, у вас не должно быть никаких проблем с std::string или std::wstring.

Проблемы начинаются, когда вы начинаете нарезку и нарезку, тогда вы должны обратить внимание на (1) границы границы кода (в UTF-8 или UTF-16) и (2) границы Графемных кластеров. Первое может быть легко обработано самостоятельно, последнее требует использования библиотеки Unicode.


Выбор std::string или std::u32string?

Если производительность является проблемой, вероятно, что std::string будет работать лучше из-за меньшего объема памяти; хотя тяжелое использование китайцев может изменить сделку. Как всегда, профиль.

Если Grapheme Clusters не проблема, то std::u32string имеет преимущество в упрощении: 1 Code Unit → 1 Code Point означает, что вы не можете случайно разделить std::basic_string очки, и все функции std::basic_string работают из коробка.

Если вы взаимодействуете с программным обеспечением, принимающим std::string или char*/char const*, тогда придерживайтесь std::string чтобы избежать обратных конверсий. Это будет боль иначе.


UTF-8 в std::string.

UTF-8 действительно работает довольно хорошо в std::string.

Большинство операций работают из коробки, потому что кодирование UTF-8 самосинхронизируется и обратно совместимо с ASCII.

Из-за того, что коды кода закодированы, поиск точки кода не может случайно совпадать с серединой другой точки кода:

  • str.find('\n') работает,
  • str.find("...") работает для сопоставления байта байтом 1,
  • str.find_first_of("\r\n") работает при поиске символов ASCII.

Аналогично, regex должно работать в основном из коробки. Поскольку последовательность символов ("haha") - это всего лишь последовательность байтов ("哈"), основные шаблоны поиска должны работать из коробки.

Однако будьте осторожны в классах символов (например, [:alphanum:]), поскольку в зависимости от аромата и реализации регулярного выражения он может или не может совпадать с символами Unicode.

Аналогичным образом, будьте осторожны с применением повторителей к "персонажам", отличным от ASCII, "哈?" может считать только последний байт необязательным; используйте круглые скобки для четкого определения повторяющейся последовательности байтов в таких случаях: "(哈)?" ,

1 Ключевыми понятиями для поиска являются нормализация и сопоставление; это влияет на все операции сравнения. std::string всегда будет сравнивать (и, следовательно, сортировать) байты по байтам, независимо от правил сравнения, специфичных для языка или использования. Если вам нужно обрабатывать полную нормализацию/сопоставление, вам нужна полная библиотека Unicode, такая как ICU.

Ответ 2

Оба std::string и std::wstring должны использовать кодировку UTF для представления Unicode. В macOS, в частности, std::string - UTF-8 (8-разрядные кодовые единицы), а std::wstring - UTF-32 (32-разрядные кодовые единицы); обратите внимание, что размер wchar_t зависит от платформы.

Для обоих size отслеживает количество блоков кода вместо количества кодовых точек или кластеров графемы. (Кодовая точка - это единый объект Unicode, один или несколько из которых образуют кластер графем. Кластеры Grapheme - это видимые символы, с которыми пользователи взаимодействуют, например буквы или эможи.)

Хотя я не знаком с представлением юникода китайцев, очень возможно, что при использовании UTF-32 количество блоков кода часто очень близко к числу кластеров графемы. Очевидно, однако, что это происходит за счет использования памяти размером до 4 раз.

Наиболее точным решением было бы использовать библиотеку Unicode, такую как ICU, для расчета свойств Unicode, которые вы используете.

Наконец, строки UTF на человеческих языках, которые не используют комбинированные символы, обычно хорошо справляются с find/regex. Я не уверен в китайском, но английский - один из них.

Ответ 3

std::string и друзья кодируют-агностик. Единственная разница между std::wstring и std::string wchar_t в том, что std::wstring использует wchar_t как отдельный элемент, а не char. Для большинства компиляторов последний является 8-битным. Первый должен быть достаточно большим для хранения любого символа Юникода, но на практике в некоторых системах это не так (например, в компиляторе Microsoft используется 16-разрядный тип). Вы не можете хранить UTF-8 в std::wstring; это не то, для чего он предназначен. Он разработан как эквивалент UTF-32 - строка, в которой каждый элемент является одним кодовым кодом Unicode.

Если вы хотите индексировать строки UTF-8 по кодовому набору Unicode или составленному символу юникода (или какой-либо другой вещи), подсчитайте длину строки UTF-8 в кодовых точках Unicode или каком-либо другом объекте unicode или найдите по коду Unicode, вы нужно будет использовать что-то другое, кроме стандартной библиотеки. ICU является одной из библиотек в этой области; могут быть и другие.

Что-то, что, вероятно, стоит отметить, так это то, что если вы ищете символы ASCII, вы можете в основном обрабатывать поток UTF-8, как если бы он был байтовым. Каждый символ ASCII кодирует то же самое в UTF-8, как и в ASCII, и каждый многобайтовый блок в UTF-8 гарантированно не содержит никаких байтов в диапазоне ASCII.