Наскільки добре підтримується Unicode в C ++ 11?


183

Я читав і чув, що C ++ 11 підтримує Unicode. Кілька питань з цього приводу:

  • Наскільки добре стандартна бібліотека C ++ підтримує Unicode?
  • Робить std::stringте, що повинно?
  • Як я ним користуюся?
  • Де можливі проблеми?

19
"Чи виконує std :: string те, що слід?" Як ви думаєте, що це має робити?
Р. Мартіньо Фернандес

2
Я використовую utfcpp.sourceforge.net для моїх потреб utf8. Це простий файл заголовка, який забезпечує ітератори для рядків Unicode.
fscan

2
std :: string повинен зберігати байти, тобто послідовність кодової одиниці кодування UTF-8. Так, це робиться саме так, з початку. utf8everywhere.org
Павло Радзівіловський

3
Найбільші потенційні проблеми з підтримкою Unicode полягають у Unicode та його використанні в самих інформаційних технологіях. Unicode не підходить (і не призначений) для того, для чого він використовується. Unicode призначений для відтворення всіх можливих гліфів, написаних десь кимось, у певний час з кожним можливим і педантичним нюансом, включаючи 3 або 4 різних значення та 3 або 4 різних способу складання одного і того ж гліфа. Це не призначене для того, щоб бути корисним для використання у повсякденній мові, і воно не призначене бути застосовним або піддаватися легкій чи однозначній обробці.
Деймон

11
Так, він призначений для використання у повсякденній мові. Моє принаймні. І ваш, ймовірно, теж. Просто виходить, що обробка людського тексту загальним способом - дуже складне завдання. Неможливо навіть однозначно визначити, що таке персонаж. Загальне відтворення гліфів навіть насправді не є частиною статуту Unicode.
Жан-Деніс Муйс

Відповіді:


267

Наскільки добре стандартна бібліотека C ++ підтримує унікод?

Страшенно.

Швидкий сканування через бібліотечні засоби, які можуть забезпечити підтримку Unicode, дає мені цей список:

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

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

Робить std::stringте, що повинно?

Так. Відповідно до стандарту C ++, це і що std::stringповинні робити його брати та сестри:

Шаблон класу basic_stringописує об'єкти, які можуть зберігати послідовність, що складається з різної кількості довільних об'єктів, подібних до знаків, з першим елементом послідовності в нульовому положенні.

Що ж, std::stringце просто чудово. Це забезпечує будь-який функціонал Unicode? Немає.

Чи слід? Напевно, ні. std::stringчудово, як послідовність charоб’єктів. Це корисно; єдиний роздратування полягає в тому, що це дуже низький рівень перегляду тексту, а стандартний C ++ не забезпечує вищого рівня.

Як я ним користуюся?

Використовуйте його як послідовність charоб’єктів; роблячи вигляд, що це щось інше, обов'язково закінчиться болем.

Де можливі проблеми?

Повсюдно? Подивимось ...

Бібліотека струн

Бібліотека рядків надає нам basic_string, що є лише послідовністю того, що стандарт називає "об'єктами, схожими на шар". Я називаю їх кодовими одиницями. Якщо ви хочете переглянути текст на високому рівні, це не те, що ви шукаєте. Це перегляд тексту, придатного для серіалізації / десеріалізації / зберігання.

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

Локалізація бібліотеки

Бібліотека локалізації все ще вважає, що один з цих "подібних до об'єктів об'єктів" дорівнює одному "символу". Це, звичайно, нерозумно, і унеможливлює неможливість роботи багатьох речей поза деякими невеликими підмножинами Unicode, як 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, які, наприклад, не достатньо хороший для деяких німецьких локалей: «ß» uppercases до «СС» ☦ але 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 як байтів є досить хорошою: вона підтримує виявлення витривалості у BOM або чіткий вибір у коді. Він також підтримує отримання продукції з та без BOM.

Є ще кілька цікавих можливостей перетворення. Немає можливості десеріалізації з потоку байтів або рядка 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!" тримайте your . Усі версії Windows з Windows 2000 використовують UTF-16.

Так, я знаю про Groeses Eszett (ẞ), але навіть якщо ви змінили всі німецькі мови протягом ночі, щоб мати ß великі регістри на ẞ, все ще існує маса інших випадків, коли це не вдасться. Спробуйте верхній корпус U + FB00 ʟᴀᴛɪɴ sᴍᴀʟʟ ʟɪɢᴀᴛᴜʀᴇ ғғ. Немає ʟᴀᴛɪɴ ᴄᴀᴘɪᴛᴀʟ ʟɪɢᴀᴛᴜʀᴇ ғғ; це просто великі регістри до двох Fs. Або U + 01F0 ʟᴀᴛɪɴ sᴍᴀʟʟ ʟᴇᴛᴛᴇʀ ᴊ ᴡɪᴛʜ ᴄᴀʀᴏɴ; немає заздалегідь складеного капіталу; це просто великі регістри до великої J та комбінуючого карона.


26
Чим більше я читаю про це, тим більше в мене виникає відчуття, щоб нічого не розуміти у всьому цьому. Я прочитав більшість цього матеріалу пару місяців тому, і досі відчуваю, що я відкриваю все це знову і знову ... Щоб не просто для мого бідного мозку, який зараз трохи болить, всі ці поради щодо utf8everywhere досі діють, правильно? Якщо я "просто" хочу, щоб мої користувачі могли відкривати та писати файли незалежно від їх системних налаштувань, я можу запитати їх ім'я файлу, зберігати його в строці std :: і все має працювати належним чином навіть у Windows? Вибачте, що запитуєте (знову) ...
Uflex

5
@Uflex Все, що ти справді можеш зробити з std :: string - це трактувати це як двійкову крапку. У правильній реалізації Unicode ні внутрішні (оскільки вони приховані глибоко в деталях реалізації), ні зовнішні кодування не мають значення (ну, сорта, вам все ще потрібно мати кодер / декодер).
Cat Plus Plus

3
@Uflex можливо. Я не знаю, чи корисна наступна порада, яку ви не розумієте.
Р. Мартіньо Фернандес

1
Є пропозиція щодо підтримки Unicode в C ++ 2014/17. Однак це 1, може бути, 4 роки і зараз мало корисного. open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3572.html
graham.reeds

20
@ graham.reeds ха-ха, дякую, але я про це знав. Перевірте розділ «Подяки»;)
Р. Мартіньо Фернандес

40

Unicode не підтримується Стандартною бібліотекою (для будь-якого розумного значення підтримуваного).

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

Якщо вам потрібно лише зберігати і катетувати краплі , це працює досить добре; але як тільки ви захочете функціонувати Unicode (кількість точок коду , кількість графем тощо), вам не пощастить.

Єдина всебічна бібліотека, про яку я знаю для цього, - це ICU . Хоча інтерфейс C ++ був похідний від Java, тому він далеко не ідіоматичний.


2
Як щодо Boost.Locale ?
Uflex

11
@Uflex: зі сторінки, яку ви пов’язали Для досягнення цієї мети Boost.Locale використовує найсучаснішу бібліотеку Unicode та локалізації: ICU - Міжнародні компоненти для Unicode.
Матьє М.

1
Boost.Locale підтримує інші не ICU движки, дивіться тут: boost.org/doc/libs/1_53_0/libs/locale/doc/html / ...
Superfly Jon

@SuperflyJon: Щоправда, але згідно з цією ж сторінкою, підтримка Unicode непрограшних мікстур "сильно обмежена".
Матьє М.

24

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

Те, що ви не можете зробити std::stringдля UTF-8, це отримати довжину в кодових точках. std::string::size()покаже вам довжину рядка в байтах , яка дорівнює лише кількості точок коду, коли ви знаходитесь у підмножині ASCII UTF-8.

Якщо вам потрібно працювати з рядками UTF-8 на рівні кодової точки (тобто не просто зберігати та друкувати їх) або якщо ви маєте справу з UTF-16, який, ймовірно, має багато внутрішніх нульових байтів, вам потрібно переглянути типи рядків з широкими символами.


3
std::stringможна кидати в iostreams із вбудованими нулями просто чудово.
Р. Мартіньо Фернандес

3
Це цілком призначено. Це зовсім не ламається, c_str()тому що size()все ще працює. Перерваються лише зламані API (тобто ті, які не можуть обробити вбудовані нулі, як більшість країн С).
Р. Мартіньо Фернандес

1
Вбудовані нулі розбиваються c_str()через те c_str(), що передбачається повернути дані як завершений нуль рядком C ---, що неможливо, через те, що в рядки C не можуть бути вбудовані нулі.
uckelman

4
Більше не. c_str()тепер просто повертає те саме, що є data(), тобто все це. API, що приймає розмір, може споживати його. API, які не можуть, не можуть.
Р. Мартіньо Фернандес

6
З невеликою різницею, яка c_str()гарантує, що за результатом слідує NUL-подібний об'єкт, і я не думаю, що data()це робить. Ні, схоже, data()зараз це теж. (Звичайно, це не обов’язково для API, які споживають розмір, а не виводять його з пошуку термінатора)
Ben Voigt

8

C ++ 11 має пару нових типів рядкових рядків для Unicode.

На жаль, підтримка в стандартній бібліотеці нерівномірних кодувань (як UTF-8) все ще погана. Наприклад, немає жодного приємного способу отримати довжину (у кодових точках) рядка UTF-8.


Тож чи все ж нам потрібно використовувати std :: wstring для імен файлів, якщо ми хочемо підтримувати не латинські мови? Тому що нові літеральні рядки тут не дуже допомагають, оскільки рядок зазвичай надходить від користувача ...
Uflex

7
@Uflex std::stringможе без проблем утримувати рядок UTF-8, але, наприклад, lengthметод повертає кількість байтів у рядку, а не кількість кодових точок.
Якийсь програміст чувак

8
Якщо чесно, то отримання довжини в кодових точках рядка не має багатьох застосувань. Довжина в байтах може бути використана, наприклад, для правильного виділення буферів.
Р. Мартиньо Фернандес

2
Кількість точок коду в рядку UTF-8 - не дуже цікаве число: його можна записати ñяк "LATIN SMALL LETTER N With TILDE" (U + 00F1) (що є однією кодовою точкою) або "LATIN SMALL LETTER N" ( U + 006E), а потім 'COMBINING TILDE' (U + 0303), що є двома кодовими точками.
Мартін Боннер підтримує Моніку

Всі ці коментарі про те, що "вам цього не потрібно, і вам не потрібно, що" як "кількість кодів неважливо" і т. Д., Звучить для мене трохи непослушно. Після того, як ви пишете синтаксичний аналіз, який повинен розбирати вихідний код utf8 сорту, залежить від конкретизації аналізатора, вважає він чи ні LATIN SMALL LETTER N' == (U+006E) followed by 'COMBINING TILDE' (U+0303).
BitTickler

4

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

Це може бути найзручніший спосіб "поводження" з рядками utf8 (тобто без нормалізації унікоду та подібних матеріалів). Ви комфортно працюєте над кодовими точками , тоді як ваша рядок залишається закодованою в кодованому довжиною chars.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.