Как использовать 3- и 4-байтовые символы Unicode со стандартными строками C++?

В стандартном C++ у нас есть char и wchar_t для хранения символов. char может хранить значения от 0x00 до 0xFF. И wchar_t может хранить значения между 0x0000 и 0xFFFF. std::string использует char, поэтому может хранить только 1-байтовые символы. std::wstring использует wchar_t, поэтому он может хранить символы шириной до 2 байт. Это то, что я знаю о строках в C++. Пожалуйста, поправьте меня, если я сказал что-то не так до этого момента.

Я прочитал статью о UTF-8 в Википедии и узнал, что некоторые символы занимают до 4 байтов пространства. Например, китайский символ ???? имеет кодовую точку Unicode 0x24B62, которая занимает 3 байта в памяти.

Есть ли строковый контейнер STL для работы с такими символами? Я ищу что-то вроде std::string32. Также у нас было main() для точки входа ASCII, wmain() для точки входа с поддержкой 16-битных символов; какую точку входа мы используем для 3- и 4-байтового кода с поддержкой Unicode?

Не могли бы вы добавить крошечный пример?

(Моя ОС: Windows 7 x64)


person hkBattousai    schedule 28.09.2012    source источник
comment
Какая ОС? Некоторые намного красивее других...   -  person BoBTFish    schedule 28.09.2012


Ответы (5)


Сначала вам нужно лучше понять Unicode. Конкретные ответы на ваши вопросы находятся внизу.

Концепции

Вам нужен более тонкий набор концепций, чем требуется для очень простой обработки текста, как преподают на вводных курсах по программированию.

  • байт
  • кодовая единица
  • кодовая точка
  • абстрактный персонаж
  • воспринимаемый пользователем персонаж

Байт — это наименьшая адресуемая единица памяти. Сегодня обычно 8 бит, способных хранить до 256 различных значений. По определению char — это один байт.

Единица кода — это наименьшая единица данных фиксированного размера, используемая для хранения текста. Когда вам на самом деле не важно содержание текста, и вы просто хотите скопировать его куда-нибудь или подсчитать, сколько памяти использует текст, тогда вам важны единицы кода. В противном случае единицы кода бесполезны.

Кодовая точка представляет отдельный член набора символов. Какие бы «символы» ни находились в наборе символов, всем им присваивается уникальный номер, и всякий раз, когда вы видите закодированное конкретное число, вы знаете, с каким членом набора символов вы имеете дело.

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

Персонажи воспринимаются пользователями так, как они звучат; то, что пользователь считает символом в любой лингвистической системе, которую он использует.

В прежние времена char представляло все эти вещи: char по определению является байтом, в char* строках единицами кода являются chars, наборы символов были небольшими, поэтому 256 значений, представляемых char, было достаточно для представления каждого члена, а Поддерживаемые лингвистические системы были простыми, поэтому члены наборов символов в основном представляли символы, которые пользователи хотели использовать напрямую.

Но этой простой системы с char, представляющей почти все, было недостаточно для поддержки более сложных систем.


Первая проблема, с которой столкнулись, заключалась в том, что некоторые языки используют гораздо больше, чем 256 символов. Так были введены «широкие» символы. Широкие символы по-прежнему использовали один тип для представления четырех из вышеперечисленных понятий, кодовых единиц, кодовых точек, абстрактных символов и символов, воспринимаемых пользователем. Однако широкие символы больше не являются одиночными байтами. Считалось, что это самый простой метод поддержки больших наборов символов.

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

Однако оказывается, что многие лингвистические системы не так просты. В некоторых системах имеет смысл не обязательно представлять каждый воспринимаемый пользователем символ одним абстрактным символом в наборе символов. В результате текст, использующий набор символов Unicode, иногда представляет воспринимаемые пользователем символы с использованием нескольких абстрактных символов или использует один абстрактный символ для представления нескольких воспринимаемых пользователем символов.

У широких символов есть еще одна проблема. Поскольку они увеличивают размер единицы кода, они увеличивают пространство, используемое для каждого символа. Если кто-то хочет иметь дело с текстом, который может быть адекватно представлен единицами однобайтового кода, но должен использовать систему расширенных символов, тогда объем используемой памяти выше, чем в случае единиц однобайтового кода. Таким образом, желательно, чтобы широкие символы не были слишком широкими. В то же время широкие символы должны быть достаточно широкими, чтобы обеспечить уникальное значение для каждого члена набора символов.

В настоящее время Юникод содержит около 100 000 абстрактных символов. Оказывается, для этого требуются широкие символы, которые шире, чем большинство людей хотят использовать. В результате система широких символов; где кодовые единицы размером более одного байта используются для непосредственного хранения значений кодовой точки, оказывается нежелательным.

Подводя итог, можно сказать, что изначально не было необходимости различать байты, кодовые единицы, кодовые точки, абстрактные символы и воспринимаемые пользователем символы. Однако со временем возникла необходимость различать каждое из этих понятий.


Кодировки

До этого текстовые данные было просто хранить. Каждый воспринимаемый пользователем символ соответствовал абстрактному символу, который имел значение кодовой точки. Было так мало символов, что 256 значений было достаточно. Таким образом, можно просто сохранить номера кодовых точек, соответствующие желаемым символам, воспринимаемым пользователем, непосредственно в виде байтов. Позже, с широкими символами, значения, соответствующие воспринимаемым пользователем символам, сохранялись непосредственно как целые числа большего размера, например, 16 бит.

Но поскольку для хранения текста Unicode таким образом потребуется больше памяти, чем люди готовы тратить (три или четыре байта на каждый символ), «кодировки» Unicode сохраняют текст не путем непосредственного сохранения значений кодовых точек, а с помощью обратимой функции для вычисления некоторых значений. количество значений кодовых единиц для хранения для каждой кодовой точки.

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

Это означает, что общий текст обычно может быть сохранен с кодировкой UTF-8, используя меньше памяти, чем 16-битные схемы символов, но также и то, что сохраненные числа не обязательно соответствуют непосредственно значениям кодовой точки абстрактных символов. Вместо этого, если вам нужно знать, какие абстрактные символы хранятся, вы должны «декодировать» сохраненные единицы кода. И если вам нужно знать символы, воспринимаемые пользователем, вам нужно дополнительно преобразовать абстрактные символы в символы, воспринимаемые пользователем.

Существует множество различных кодировок, и для преобразования данных с использованием этих кодировок в абстрактные символы необходимо знать правильный метод декодирования. Сохраненные значения фактически бессмысленны, если вы не знаете, какая кодировка использовалась для преобразования значений кодовых точек в кодовые единицы.


Важным следствием кодирования является то, что вам нужно знать, являются ли конкретные манипуляции с закодированными данными действительными или значимыми.

Например, если вы хотите получить «размер» строки, считаете ли вы байты, единицы кода, абстрактные символы или воспринимаемые пользователем символы? std::string::size() считает единицы кода, и если вам нужно другое количество, вам нужно использовать другой метод.

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

Ответы

Сегодня char и wchar_t можно считать только кодовыми единицами. Тот факт, что char состоит только из одного байта, не мешает ему представлять кодовые точки, занимающие два, три или четыре байта. Вам просто нужно последовательно использовать два, три или четыре символа char. Вот как UTF-8 должен был работать. Точно так же платформы, которые используют два байта wchar_t для представления UTF-16, просто используют два wchar_t подряд, когда это необходимо. Фактические значения char и wchar_t по отдельности не представляют кодовые точки Unicode. Они представляют значения кодовых единиц, полученные в результате кодирования кодовых точек. Например. Кодовая точка Unicode U+0400 закодирована в две кодовые единицы в UTF-8 -> 0xD0 0x80. Кодовая точка Юникода U+24B62 аналогичным образом кодируется четырьмя кодовыми единицами 0xF0 0xA4 0xAD 0xA2.

Таким образом, вы можете использовать std::string для хранения данных в кодировке UTF-8.

В Windows main() поддерживает не только ASCII, но и любую кодировку системы char. К сожалению, Windows не поддерживает UTF-8 как системную кодировку char, как это делают другие платформы, поэтому вы ограничены устаревшими кодировками, такими как cp1252 или любой другой, на использование которой настроена ваша система. Однако вы можете использовать вызов API Win32 для прямого доступа к параметрам командной строки UTF-16 вместо использования параметров main()s argc и argv. См. GetCommandLineW() и < a href="https://msdn.microsoft.com/en-us/library/windows/desktop/bb776391(v=vs.85).aspx" rel="noreferrer">CommandLineToArgvW.

Параметр argv wmain() полностью поддерживает Unicode. 16-битные кодовые единицы, хранящиеся в wchar_t в Windows, являются кодовыми единицами UTF-16. Windows API изначально использует кодировку UTF-16, поэтому с ней довольно легко работать в Windows. wmain() не является стандартным, поэтому полагаться на него нельзя.

person bames53    schedule 28.09.2012

Windows использует UTF-16. Любая кодовая точка в диапазоне от U+0000 до U+D7FF и от U+E000 до U+FFFF будет сохранена напрямую; любое значение за пределами этих диапазонов будет разделено на два 16-битных значения в соответствии с правилами кодирования UTF-16.

Например, 0x24B62 будет закодирован как 0xd892,0xdf62.

Вы можете преобразовать строки для работы с ними любым удобным для вас способом, но API Windows по-прежнему будет требовать и предоставлять UTF-16, так что, вероятно, это будет наиболее удобно.

person Mark Ransom    schedule 28.09.2012

Размер и значение wchar_t определяются реализацией. В Windows это 16 бит, как вы говорите, в Unix-подобных системах это часто 32 бит, но не всегда.

Если на то пошло, компилятору разрешено делать что-то свое и выбирать для wchar_t размер, отличный от того, что говорит система — он просто не будет совместим с ABI с остальной частью системы.

С++ 11 предоставляет std::u32string для представления строк кодовых точек Unicode. Я полагаю, что достаточно свежие компиляторы Microsoft включают его. Его использование несколько ограничено, поскольку системные функции Microsoft ожидают 16-битных символов (также известных как UTF-16le), а не 32-битных кодовых точек Unicode (также известных как UTF-32, UCS-4).

Однако вы упоминаете UTF-8: данные в кодировке UTF-8 могут храниться в обычном формате std::string. Конечно, поскольку это кодировка переменной длины, вы не можете получить доступ к кодовым точкам Unicode по индексу, вы можете получить доступ только к байтам по индексу. Но обычно вы пишете свой код так, чтобы в любом случае не нужно было обращаться к кодовым точкам по индексу, даже если вы используете u32string. Кодовые точки Unicode не соответствуют 1-1 печатным символам («графемам») из-за существования комбинированных меток в Unicode, так что многие маленькие трюки, которые вы играете со строками при обучении программированию (переворачивание их, поиск подстрок) не так легко работать с данными Unicode, независимо от того, где вы их храните.

Символ ????, как вы говорите, Ⓐ2. Это кодировка UTF-8 в виде последовательности четырех байтов, а не трех: F0 A4 AD A2. Перевод между кодированными данными UTF-8 и кодовыми точками Unicode требует усилий (по общему признанию, не так уж много усилий, и библиотечные функции сделают это за вас). Лучше всего рассматривать «закодированные данные» и «данные Unicode» как отдельные вещи. Вы можете использовать любое представление, которое считаете наиболее удобным, вплоть до того момента, когда вам нужно (например) отобразить текст на экране. В этот момент вам нужно (повторно) закодировать его в кодировку, которую понимает ваш выходной пункт назначения.

person Steve Jessop    schedule 28.09.2012
comment
(всегда?) Нет. Например, AIX использует 16-битный wchar_t и UTF-16. - person bames53; 28.09.2012
comment
Хорошим примером является оригинальный Andriod NDK, в котором использовался 8-битный тип wchar_t. - person Captain Obvlious; 28.09.2012

В стандартном С++ у нас есть char и wchar_t для хранения символов? char может хранить значения от 0x00 до 0xFF. А wchar_t может хранить значения от 0x0000 до 0xFFFF.

Не совсем:

sizeof(char)     == 1   so 1 byte per character.
sizeof(wchar_t)  == ?   Depends on your system 
                        (for unix usually 4 for Windows usually 2).

Символы Unicode занимают до 4 байтов пространства.

Не совсем. Юникод это не кодировка. Юникод — это стандарт, который определяет, что такое каждая кодовая точка, а кодовые точки ограничены 21 битом. Первые 16 бит определяют позицию символа на кодовой равнине, а следующие 5 бит определяют, на какой равнине находится символ.

Существует несколько кодировок юникода (наиболее распространены UTF-8, UTF-16 и UTF-32), так вы храните символы в памяти. Между этими тремя есть практические различия.

    UTF-8:   Great for storage and transport (as it is compact)
             Bad because it is variable length
    UTF-16:  Horrible in nearly all regards
             It is always large and it is variable length
             (anything not on the BMP needs to be encoded as surrogate pairs)
    UTF-32:  Great for in memory representations as it is fixed size
             Bad because it takes 4 bytes for each character which is usually overkill

Лично я использую UTF-8 для транспортировки и хранения и UTF-32 для представления текста в памяти.

person Martin York    schedule 28.09.2012
comment
А что вы используете для представления последовательности кластеров графем? ;-п - person Steve Jessop; 28.09.2012
comment
@SteveJessop: Да, кластеры графем (трудно произносимое слово) фактически располагают несколько глифов вместе в одном месте и представляют собой панель. - person Martin York; 28.09.2012
comment
Для тех, кто смотрит в недоумении: проблема в том, что полностью общая обработка текста Unicode, например, возможность перевернуть строку или напечатать только первые 10 символов, на самом деле требует обработки UTF-32 как кодировки переменной длины. - person Steve Jessop; 28.09.2012

char и wchar_t — не единственные типы данных, используемые для текстовых строк. C++11 представляет новые типы данных char16_t и char32_t и соответствующие определения типов STL std::u16string и std::u32string для std::basic_string, чтобы устранить неоднозначность типа wchar_t, который имеет разные размеры и кодировки на разных платформах. wchar_t является 16-битным на некоторых платформах, подходящим для кодировки UTF-16, но 32-битным на других платформах, подходящим для кодировки UTF-32. char16_t — это 16-битный код и кодировка UTF-16, а char32_t — 32-битный код и кодировка UTF-32 на всех платформах.

person Remy Lebeau    schedule 03.10.2012