Я читав і чув, що C ++ 11 підтримує Unicode. Кілька питань з цього приводу:
- Наскільки добре стандартна бібліотека C ++ підтримує Unicode?
- Робить
std::string
те, що повинно? - Як я ним користуюся?
- Де можливі проблеми?
Я читав і чув, що C ++ 11 підтримує Unicode. Кілька питань з цього приводу:
std::string
те, що повинно?Відповіді:
Наскільки добре стандартна бібліотека 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; стандарт дозволяє здійснювати перетворення в обох напрямках).
codecvt_utf8<char16_t>
і codecvt_utf8<wchar_t>
де sizeof(wchar_t) == 2
;codecvt_utf8<char32_t>
, codecvt<char32_t, char, mbstate_t>
і codecvt_utf8<wchar_t>
де sizeof(wchar_t) == 4
;codecvt_utf16<char16_t>
і codecvt_utf16<wchar_t>
де sizeof(wchar_t) == 2
;codecvt_utf16<char32_t>
і codecvt_utf16<wchar_t>
де sizeof(wchar_t) == 4
;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 та комбінуючого карона.
Unicode не підтримується Стандартною бібліотекою (для будь-якого розумного значення підтримуваного).
std::string
не краще, ніж std::vector<char>
: він повністю не звертає уваги на Unicode (або будь-яке інше представлення / кодування) і просто ставиться до його вмісту як до крапки байтів.
Якщо вам потрібно лише зберігати і катетувати краплі , це працює досить добре; але як тільки ви захочете функціонувати Unicode (кількість точок коду , кількість графем тощо), вам не пощастить.
Єдина всебічна бібліотека, про яку я знаю для цього, - це ICU . Хоча інтерфейс C ++ був похідний від Java, тому він далеко не ідіоматичний.
Ви можете безпечно зберігати 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, який, ймовірно, має багато внутрішніх нульових байтів, вам потрібно переглянути типи рядків з широкими символами.
std::string
можна кидати в iostreams із вбудованими нулями просто чудово.
c_str()
тому що size()
все ще працює. Перерваються лише зламані API (тобто ті, які не можуть обробити вбудовані нулі, як більшість країн С).
c_str()
через те c_str()
, що передбачається повернути дані як завершений нуль рядком C ---, що неможливо, через те, що в рядки C не можуть бути вбудовані нулі.
c_str()
тепер просто повертає те саме, що є data()
, тобто все це. API, що приймає розмір, може споживати його. API, які не можуть, не можуть.
c_str()
гарантує, що за результатом слідує NUL-подібний об'єкт, і я не думаю, що data()
це робить. Ні, схоже, data()
зараз це теж. (Звичайно, це не обов’язково для API, які споживають розмір, а не виводять його з пошуку термінатора)
C ++ 11 має пару нових типів рядкових рядків для Unicode.
На жаль, підтримка в стандартній бібліотеці нерівномірних кодувань (як UTF-8) все ще погана. Наприклад, немає жодного приємного способу отримати довжину (у кодових точках) рядка UTF-8.
std::string
може без проблем утримувати рядок UTF-8, але, наприклад, length
метод повертає кількість байтів у рядку, а не кількість кодових точок.
ñ
як "LATIN SMALL LETTER N With TILDE" (U + 00F1) (що є однією кодовою точкою) або "LATIN SMALL LETTER N" ( U + 006E), а потім 'COMBINING TILDE' (U + 0303), що є двома кодовими точками.
LATIN SMALL LETTER N'
== (U+006E) followed by 'COMBINING TILDE' (U+0303)
.
Однак є досить корисна бібліотека під назвою tiny-utf8 , яка в основному є заміною для std::string
/ std::wstring
. Він спрямований на заповнення прогалини все ще відсутнього класу контейнерів utf8-string.
Це може бути найзручніший спосіб "поводження" з рядками utf8 (тобто без нормалізації унікоду та подібних матеріалів). Ви комфортно працюєте над кодовими точками , тоді як ваша рядок залишається закодованою в кодованому довжиною char
s.