Насколько хорошо поддерживается Unicode в С++ 11?

Я читал и слышал, что С++ 11 поддерживает Unicode. Несколько вопросов по этому поводу:

  • Насколько хорошо стандартная библиотека С++ поддерживает Unicode?
  • Выполняет ли std::string то, что нужно?
  • Как его использовать?
  • Где потенциальные проблемы?

Ответ 1

Насколько хорошо стандартная библиотека C++ поддерживает юникод?

Жутко.

Быстрый просмотр библиотечных средств, которые могут обеспечить поддержку Unicode, дает мне этот список:

  • Библиотека строк
  • Библиотека локализации
  • Библиотека ввода/вывода
  • Библиотека регулярных выражений

Я думаю, что все, кроме первого, оказывают ужасную поддержку. Я вернусь к этому более подробно после короткого обхода других ваших вопросов.

Делает ли std::string то, что должен?

Да. Согласно стандарту C++, вот что должен делать std::string и его братья и сестры:

Шаблон класса basic_string описывает объекты, которые могут хранить последовательность, состоящую из различного числа произвольных подобных basic_string объектов с первым элементом последовательности в нулевой позиции.

Ну, std::string делает это просто отлично. Предоставляет ли это какие-либо специфичные для Unicode функции? Нет.

Должно ли это? Возможно нет. std::string подходит как последовательность объектов char. Это полезно; Единственное раздражение в том, что это очень низкоуровневое представление текста, а стандарт C++ не обеспечивает более высокого уровня.

Как мне это использовать?

Используйте его как последовательность объектов char; притворяться, что это что-то еще, должно закончиться болью.

Где потенциальные проблемы?

Повсюду? Давай посмотрим...

Библиотека строк

Библиотека строк предоставляет нам basic_string, которая является просто последовательностью того, что стандарт называет "объектами типа char". Я называю их кодовыми единицами. Если вы хотите просмотреть текст на высоком уровне, это не то, что вы ищете. Это вид текста, подходящего для сериализации/десериализации/хранения.

Он также предоставляет некоторые инструменты из библиотеки C, которые можно использовать для преодоления разрыва между узким миром и миром Unicode: c16rtomb/mbrtoc16 и c32rtomb/mbrtoc32.

Библиотека локализации

Библиотека локализации по-прежнему считает, что один из этих "похожих на символы" объектов равен одному "символу". Это, конечно, глупо и делает невозможным правильную работу многих вещей, кроме небольшого подмножества Юникода, такого как ASCII.

Рассмотрим, например, что стандарт называет "удобными интерфейсами" в заголовке <locale>:

template <class charT> bool isspace (charT c, const locale& loc);
template <class charT> bool isprint (charT c, const locale& loc);
template <class charT> bool iscntrl (charT c, const locale& loc);
// ...
template <class charT> charT toupper(charT c, const locale& loc);
template <class charT> charT tolower(charT c, const locale& loc);
// ...

Как вы ожидаете, что какая-либо из этих функций правильно классифицирует, скажем, U + 1F34C ʙᴀɴᴀɴᴀ, как в u8"🍌" или u8"\U0001F34C"? Нет никакого способа, которым это когда-либо будет работать, потому что эти функции принимают только одну единицу кода в качестве входных данных.

Это может работать с соответствующей локалью, если вы использовали только char32_t: U'\U0001F34C' - это единица кода в UTF-32.

Однако это по-прежнему означает, что вы получаете только простые преобразования корпусов с помощью toupper и tolower, которые, например, не подходят для некоторых немецких языков: верхние регистры "ß" - "SS" ☦, но toupper может возвращать только одну единицу кода символа.

Далее, wstring_convert/wbuffer_convert и стандартные аспекты преобразования кода.

wstring_convert используется для преобразования между строками в одной заданной кодировке в строки в другой заданной кодировке. В этом преобразовании участвуют два строковых типа, которые в стандарте называются байтовой строкой и широкой строкой. Поскольку эти термины действительно вводят в заблуждение, я предпочитаю использовать "сериализованный" и "десериализованный" соответственно †.

Кодировки для преобразования определяются с помощью codecvt (фасета преобразования кода), передаваемого в качестве аргумента типа шаблона в wstring_convert.

wbuffer_convert выполняет аналогичную функцию, но как широкий десериализованный буфер потока, который оборачивает байтовый сериализованный буфер потока. Любой ввод/вывод выполняется через базовый байтовый сериализованный потоковый буфер с преобразованиями в и из кодировок, заданных аргументом codecvt. Запись сериализуется в этот буфер, а затем записывает из него, а чтение читает в буфер, а затем десериализуется из него.

Стандарт предоставляет некоторые шаблоны классов codecvt для использования со следующими средствами: codecvt_utf8, codecvt_utf16, codecvt_utf8_utf16 и некоторые специализации codecvt. Вместе эти стандартные аспекты обеспечивают все следующие преобразования. (Примечание: в следующем списке кодировка слева всегда является сериализованной строкой /streambuf, а кодировка справа всегда десериализованной строкой /streambuf; стандарт допускает преобразования в обоих направлениях).

  • UTF-8 UCS-2 с codecvt_utf8<char16_t> и codecvt_utf8<wchar_t> где sizeof(wchar_t) == 2;
  • UTF-8 ↔ UTF-32 с codecvt_utf8<char32_t>, codecvt<char32_t, char, mbstate_t> и codecvt_utf8<wchar_t> где sizeof(wchar_t) == 4;
  • UTF-16 ↔ UCS-2 с codecvt_utf16<char16_t> и codecvt_utf16<wchar_t> где sizeof(wchar_t) == 2;
  • UTF-16 ↔ UTF-32 с codecvt_utf16<char32_t> и codecvt_utf16<wchar_t> где sizeof(wchar_t) == 4;
  • UTF-8 ↔ UTF-16 с codecvt_utf8_utf16<char16_t>, codecvt<char16_t, char, mbstate_t> и codecvt_utf8_utf16<wchar_t> где sizeof(wchar_t) == 2;
  • узкий, широкий с codecvt<wchar_t, char_t, mbstate_t>
  • нет операции с codecvt<char, char, mbstate_t>.

Некоторые из них полезны, но здесь есть много неловких вещей.

Прежде всего - святой верховный суррогат! эта схема именования является грязной.

Тогда есть много поддержки UCS-2. UCS-2 - это кодировка из Unicode 1.0, которая была заменена в 1996 году, потому что она поддерживает только базовую многоязычную плоскость. Почему комитет счел желательным сосредоточиться на кодировке, которая была заменена более 20 лет назад, я не знаю ‡. Не то, чтобы поддержка большего количества кодировок была плохой или что-то в этом роде, но UCS-2 появляется здесь слишком часто.

Я бы сказал, что char16_t явно предназначен для хранения кодовых блоков UTF-16. Тем не менее, это одна часть стандарта, которая считает иначе. codecvt_utf8<char16_t> имеет ничего общего с UTF-16. Например, wstring_convert<codecvt_utf8<char16_t>>().to_bytes(u"\U0001F34C") скомпилируется нормально, но безоговорочно завершится ошибкой: вход будет обрабатываться как строка UCS-2 u"\xD83C\xDF4C", которая не может быть преобразован в UTF-8, потому что UTF-8 не может кодировать любое значение в диапазоне 0xD800-0xDFFF.

Тем не менее, на фронте UCS-2 нет способа считывания из потока байтов UTF-16 в строку UTF-16 с этими аспектами. Если у вас есть последовательность байтов UTF-16, вы не можете десериализовать ее в строку char16_t. Это удивительно, потому что это более или менее преобразование личности. Еще более удивительным является тот факт, что существует поддержка десериализации из потока UTF-16 в строку UCS-2 с codecvt_utf16<char16_t>, которая фактически является преобразованием с потерями.

Тем не менее, поддержка UTF-16 в качестве байтов довольно хороша: она поддерживает обнаружение бесконечности в спецификации или ее явное выделение в коде. Он также поддерживает создание вывода с и без спецификации.

Есть еще несколько интересных возможностей для конвертации. Невозможно десериализовать поток байтов или строку UTF-16 в строку UTF-8, поскольку UTF-8 никогда не поддерживается в качестве десериализованной формы.

И здесь узкий/широкий мир полностью отделен от мира UTF/UCS. Не существует преобразований между узкими/широкими кодировками старого стиля и любыми кодировками Unicode.

Библиотека ввода/вывода

Библиотеку ввода/вывода можно использовать для чтения и записи текста в кодировках Unicode с использованием описанных выше средств wstring_convert и wbuffer_convert. Я не думаю, что есть еще что-то, что могло бы быть поддержано этой частью стандартной библиотеки.

Библиотека регулярных выражений

Я уже объяснил проблемы с регулярными выражениями C++ и Unicode в переполнении стека. Я не буду повторять здесь все эти пункты, а просто скажу, что регулярные выражения C++ не имеют поддержки Unicode 1-го уровня, что является минимальным условием для их использования без использования UTF-32 везде.

Это?

Да, это так. Это существующий функционал. Существует множество функций Unicode, которые нигде не встречаются, такие как алгоритмы нормализации или сегментации текста.

U + 1F4A9. Есть ли способ получить лучшую поддержку Unicode в C++?

Обычные подозреваемые: ICU и Boost.Locale.


† Неудивительно, что строка байтов - это строка байтов, т. char объектов. Однако, в отличие от литерала с широкой строкой, который всегда является массивом объектов wchar_t, "широкая строка" в этом контексте не обязательно является строкой объектов wchar_t. Фактически, стандарт никогда не определяет явно, что означает "широкая строка", поэтому нам остается только догадываться о значении использования. Поскольку стандартная терминология небрежная и запутанная, я использую свою собственную во имя ясности.

Кодировки, подобные UTF-16, могут храниться в виде последовательностей char16_t, которые затем не имеют порядка байтов; или они могут быть сохранены как последовательности байтов, которые имеют порядковый номер (каждая последовательная пара байтов может представлять различное значение char16_t зависимости от порядкового номера). Стандарт поддерживает обе эти формы. Последовательность char16_t более полезна для внутренних манипуляций в программе. Последовательность байтов - это способ обмена такими строками с внешним миром. Термины, которые я буду использовать вместо "байт" и "широкий", таким образом, "сериализуются" и "десериализуются".

‡ Если вы собираетесь сказать "но Windows!" держи свой 🐎🐎. Все версии Windows начиная с Windows 2000 используют UTF-16.

☦ Да, я знаю о grosses Eszett (ẞ), но даже если бы вам пришлось поменять все немецкие локации на ночь, чтобы прописные буквы были прописными, то есть еще множество других случаев, когда это не сработало. Попробуйте прописные буквы U + FB00 ʟᴀᴛɪɴ sᴍᴀʟʟ ʟɪɢᴀᴛᴜʀᴇ ғғ. Там нет ʟᴀᴛɪɴ ᴄᴀᴘɪᴛᴀʟ ʟɪɢᴀᴛᴜʀᴇ ғғ; это только прописные буквы до двух Fs. Или U + 01F0 ʟᴀᴛɪɴ sᴍᴀʟʟ ʟᴇᴛᴛᴇʀ ᴊ ᴡɪᴛʜ ᴄᴀʀᴏɴ; нет предварительно составленного капитала; это только заглавные буквы к заглавной букве J и объединяющему caron.

Ответ 2

Unicode не поддерживается стандартной библиотекой (для любого разумного значения поддерживается).

std::string не лучше, чем std::vector<char>: он полностью игнорирует Unicode (или любое другое представление/кодировку) и просто обрабатывает его содержимое как блок байтов.

Если вам нужно только хранить и катать капли, это работает довольно хорошо; но как только вы захотите использовать функциональность Unicode (количество точек кода, количество графем и т.д.), вам не повезло.

Единственная всеобъемлющая библиотека, о которой я знаю, это ICU. Интерфейс C++ был получен от Java, поэтому он далеко не идиоматичен.

Ответ 3

Вы можете безопасно хранить UTF-8 в std::string (или в char[] или char*, в этом отношении), потому что Unicode NUL (U + 0000) является нулевым байтом в UTF-8 и что это единственный способ, которым нулевой байт может появиться в UTF-8. Следовательно, ваши строки UTF-8 будут должным образом завершены в соответствии со всеми строковыми функциями C и C++, и вы можете перебрасывать их с помощью iostreams C++ (включая std::cout и std::cerr, до тех пор, пока Ваш регион UTF-8).

Что вы не можете сделать с помощью std::string для UTF-8, так это получить длину в кодовых точках. std::string::size() сообщит вам длину строки в байтах, которая равна только числу кодовых точек, когда вы находитесь в подмножестве ASCII UTF-8.

Если вам нужно работать со строками UTF-8 на уровне кодовой точки (т.е. не просто сохранять и распечатывать их), или если вы имеете дело с UTF-16, который, вероятно, имеет много внутренних нулевых байтов, вам нужно изучить типы строк широких символов.

Ответ 4

С++ 11 имеет пару новых литеральных типов строк для Unicode.

К сожалению, поддержка в стандартной библиотеке для неравномерных кодировок (например, UTF-8) по-прежнему плохая. Например, нет хорошего способа получить длину (в кодовых точках) строки UTF-8.

Ответ 5

Тем не менее, есть довольно полезная библиотека с именем tiny-utf8, которая по сути является заменой std::string/std::wstring. Он призван восполнить пробел в еще отсутствующем классе контейнера utf8-string.

Это может быть наиболее удобным способом работы со строками utf8 (то есть без нормализации Юникода и тому подобного). Вы комфортно работаете с кодовыми точками, в то время как ваша строка остается закодированной в кодированных char серий char.