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


82

Моя платформа - це Mac. Я новачок у C ++ і працюю над особистим проектом, який обробляє китайську та англійську мови. UTF-8 є найкращим кодуванням для цього проекту.

Я читав деякі дописи про Stack Overflow, і багато з них пропонують використовувати їх std::stringпри роботі з UTF-8 та уникати, wchar_tоскільки зараз немає char8_tUTF-8.

Тим НЕ менше, жоден з них не говорити про те , як правильно працювати з функціями , як str[i], std::string::size(), std::string::find_first_of()або , std::regexяк вони функціонують , як правило , повертає несподівані результати при зіткненні UTF-8.

Чи слід мені продовжувати std::stringабо переходити на std::wstring? Якщо я маю залишитися std::string, яка найкраща практика для вирішення вищезазначених проблем?


13
Дивіться також utf8everywhere
Калет,

3
Чому (і як ?!) Ви б використовували std::wstringз UTF-8?
Джонатан Уейклі,

6
std::string::size()це лише дивно, якщо ви очікуєте, що не зробите щось, крім повернення довжини в байтах, тобто одиниць коду (а не кількості кодових точок у рядку). І str[i]повертає i-й байт у рядку. Але це все одно буде вірно, навіть якби C ++ мав char8_tтип спеціально для UTF-8.
Джонатан Уейклі,

Це може бути дещо не в темі, але чому саме C ++? Це досить другий громадянин Mac, Apple забезпечує набагато кращу підтримку Objective-C та (нещодавно) Swift. Виходячи з того, що, здається, ви пишете програму командного рядка, ви можете поглянути на це . Тоді ви можете перестати турбуватися про безглузду підтримку С ++ для Unicode і продовжити писати свою програму. Google swift unicodeі swift regexвсе це зроблено для вас.
Пол Сандерс,

PS: що програма на насправді робити ?
Пол Сандерс,

Відповіді:


120

Глосарій Unicode

Unicode - велика і складна тема. Я не хочу заглиблюватися там занадто глибоко, проте необхідний швидкий глосарій:

  1. Точки коду : Точки коду - це основні будівельні блоки Unicode, кодова точка - це просто ціле число, що відображається у значенні . Ціла частина вміщується в 32 біти (ну, насправді 24 біти), і значення може бути буква, діакритика, пробіл, знак, смайлик, напівпрапор…, і це може бути навіть " наступна частина читається справа наліво ".
  2. Кластери графем : Кластери графем - це групи семантично пов’язаних точок коду, наприклад, прапор у unicode представлений об’єднанням двох точок коду; кожен з цих двох, ізольовано, не має значення, але об'єднаний у скупчення графем, вони представляють прапор. Кластери графем також використовуються для поєднання листа з діакритикою в деяких сценаріях.

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


UTF Primer

Потім, серія Unicode Code Points повинна бути закодована; загальними кодуваннями є 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у Windows лише 16 біт); використовувати std::u32stringзамість цього (ака std::basic_string<char32_t>).
  2. Представлення в пам'яті ( std::stringабо std::wstring) не залежить від представлення на диску (UTF-8, UTF-16 або UTF-32), тому підготуйтеся до необхідності перетворення на межі (читання та запис).
  3. Хоча 32-біт wchar_tгарантує, що одиниця коду представляє повну точку коду, вона все ще не являє собою повний кластер графем.

Якщо ви лише читаєте або складаєте рядки, у вас не повинно бути проблем із std::stringабо std::wstring.

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


Збір std::stringабо std::u32string?

Якщо продуктивність викликає занепокоєння, швидше за все, вона std::stringбуде працювати краще завдяки меншому розміру пам'яті; хоча інтенсивне використання китайської мови може змінити угоду. Як завжди, профіль.

Якщо кластери графем не є проблемою, то std::u32stringперевага полягає у спрощенні речей: 1 одиниця коду -> 1 кодова точка означає, що ви не можете випадково розділити кодові точки, а всі функції 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.


Дякуємо за чудові деталі! Я намагаюся витратити трохи часу, щоб зрозуміти все це! Про вихідні запитання, крім того str.find_first_of, str.findабо, std::regexздається, не працюють для входів, що не належать до ASCII (наприклад, "哈" або u8 "哈"), поданоstd::string str(u8"哈哈haha");
stackunderflow

4
@Edityouprofile: str.find("哈")повинен працювати (див. Ideone.com/s9i1yf ), але str.find('哈')не буде, оскільки '哈'є багатобайтовими символами. str.find_first_of("哈")не буде працювати (працює лише для шаблонів ASCII). Regex повинен чудово працювати для шаблонів ASCII; однак остерігайтеся класів символів та "повторювачів" (наприклад, "哈?"може лише умовний байт).
Matthieu M.

1
Для переносимості, як би це std::basic_string<char32_t>працювало як на * nix, так і на Windows?
Квентін

1
@ Квентін: Так. Я повинен додати його до списку альтернатив! До речі, є відмінна ЬурейеЕ: std::u32string.
Matthieu M.

1
str.find("...")str.fin worksлише якщо ви дбаєте лише про відповідність байт-за-байтом - інакше вам знадобиться належне порівняння з урахуванням нормалізації та локалі. Крім цього, це здається досить гарною відповіддю і показує, чому я ненавиджу "підтримку" Unicode, яка існує в таких мовах, як Python3.
Muzer

10

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

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

Щось, що, мабуть, варто зауважити, це те, що якщо ви шукаєте символи ASCII, ви можете здебільшого розглядати байт-сон UTF-8 так, ніби це байт-байт. Кожен символ ASCII кодує те саме в UTF-8, що і в ASCII, і кожна багатобайтова одиниця в UTF-8 гарантовано не включає жодних байтів у діапазон ASCII.


3
"різні коди для всіх членів найбільшого розширеного набору символів" означає, що один wchar_t повинен мати можливість представляти будь-яку дійсну точку коду Unicode, якщо ваш компілятор підтримує Unicode. 16 біт для цього недостатньо. UTF-16 - це багатобайтове кодування; це тут не актуально.
Джеймс Піконе,

6
Шкода в тому, що std::wstringнасправді не повинно бути багатобайтове кодування; це суть типу. Зробити його багатобайтовим кодуванням (причому поганим) просто дублює std::string, але дуже дратує, коли люди обдурюють, що їх код робить Unicode належним чином.
Джеймс Піконе,

11
@zneak це насправді винна Unicode, а не Microsoft. Вони сказали Microsoft, що символи 16-розрядні, потім Microsoft пішла і зробила їх 16-розрядними, потім вони сказали "ой, ні, вони повинні бути 20,5-розрядними". Єдина причина, чому * nixes не має тієї ж проблеми, полягає в тому, що вони взагалі не підтримували Unicode, доки не було прийнято 20,5-бітове рішення.
user253751

4
@zneak UTF-32 не є багатобайтовим кодуванням так само, як UTF-16. UTF-16 іноді вимагає декількох значень для представлення єдиних кодових точок Unicode. UTF-32 іноді вимагає декількох кодових точок Unicode для представлення окремих графем. Вони обоє хитрі, але хитрі на різних рівнях.
Джеймс Піконе,

8
@JamesPicone: "Кодування із змінною шириною", мабуть, є більш відповідним терміном, ніж "багатобайтове кодування".
user2357112 підтримує Моніку

7

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

Для обох sizeвідстежується кількість одиниць коду замість кількості кодових точок, або графемних кластерів. (Кодовою точкою називається сутність Unicode, одна або кілька з яких утворюють кластер графем. Графеми кластери - це видимі символи, з якими взаємодіють користувачі, такі як букви або смайлики.)

Хоча я не знайомий з представленням китайської мови в Unicode, цілком можливо, що коли ви використовуєте UTF-32, кількість одиниць коду часто дуже близька до кількості кластерів графем. Очевидно, однак, це відбувається за рахунок використання в 4 рази більше пам'яті.

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

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


2
Дякую за відповідь. Хоча, std::string str(u8"哈哈haha");str.find_first_of(u8"haha");здається, працює, str.find_first_of(u8"哈ha");завжди повертайте 0. І регулярний вираз, здається, теж не працює.
stackunderflow

1
@Edityouprofile, це моя помилка: я переплутав find_first_ofіз find. find_first_ofне може працювати з багатобайтовими символами.
zneak

11
" Для обох sizeвідстежує кількість кодових точок " - неправильно, він представляє кодові одиниці , а не кодові точки . Велика різниця. " замість кількості логічних символів. (Логічні символи - це одна або кілька кодових точок.) " - також більш формально відомий як кластер графем.
Remy Lebeau

2
Я не думаю, що стандарт вимагає std::string бути в UTF8, навіть якщо ми маємо скрізь UTF8 . Я припускаю, що в мейнфреймі EBCDIC може використовуватися EBCDIC дляstd::string
Базиля Старинкевича

14
std::stringне "використовує" жодного кодування, ні UTF-8, ні EBCDIC. std::stringце просто контейнер для байтів типів char. Ви можете помістити туди рядки UTF-8, або рядки ASCII, або рядки EBCDIC, або навіть двійкові дані. Кодування цих байтів (якщо такі є) визначається рештою вашої програми та тим, що ви робите із рядком, а не самим std::stringсобою.
Джонатан Уейклі,

5

Подумайте про оновлення до C ++ 20, і std::u8stringце найкраще, що ми маємо на 2019 рік для проведення UTF-8. Немає стандартних бібліотечних засобів для доступу до окремих точок коду або графемних кластерів, але принаймні ваш тип досить сильний, щоб принаймні сказати, що це справжній UTF-8.


це u8string повинен бути шлях - см stackoverflow.com/questions/56420790 / ... ... no_wide є піднімати торг досить цікаво для потокового utf8 - boost.org/doc/libs/1_74_0/libs/nowide/doc/html/ index.html
jolyon

Однозначно уникайте u8string, оскільки він погано підтримується стандартом. Ви навіть не зможете його вивести.
vitaut

0

Чи слід мені продовжувати std::stringабо переходити на std::wstring?

Я б рекомендував використовувати, std::stringоскільки wchar_tвін не є портативним, а C ++ 20 char8_tпогано підтримується стандартом і не підтримується жодними системними API (і, швидше за все, ніколи не буде через причини сумісності). На більшості платформ, включаючи macOS, у яких ви використовуєте звичайні charрядки, вже є UTF-8.

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

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