Кодировки
Распространенные заблуждения
Может показаться, что обычного восьмибитного char может не хватить, но зато
char16_t
решает эту проблему. Или что если программа работает с русским и
английским языками, то с остальными не должно возникнуть проблем. Например,
текстовый редактор может не учитывать что символ может кодироваться суррогатной
парой (об этом чуть позже) и когда нажимается backspace, то он стирает только
один code unit (и об этом тоже).
Unicode
Юникод разбивает текст на составные части и нумерует их. Например, числу 65
соответствует заглавная латинская буква 'A'. Эти числа называют code
point-ами. У code points диапазон 0...0x10FFFF (a.k.a. 17*2^16
). Уже видно,
что char16_t
не справляется. Можно было бы похранить в 32-битном числе, но это
как-то не очень. Поэтому придумывают разные способы кодировки каждого code
point. Последовательность бит, в которую мы переводим code point в какой-то
конкретной кодировке мы как-то записываем в виде последовательности нескольких
одинаковых по размеру частей. Каждая такая часть называется code unit.
UTF-16
Сначала была ucs2 которая прямо мапила code point в 16-битный code unit. Потом, когда поняли что 16 бит не хватает, придумали UTF-16, где основную часть маппинга сохранили, но какие-то маппинги признали невалидными (0xD800-0xDFFF). Та часть кодпойнтов, которая выходит за пределы валидного диапазона, кодируется уже двумя код юнитами, которые называются суррогатной парой. Получается что-то такое:
0 <= cp <= 0xD7FF или 0xE000 <= cp <= 0xFFFF
Такие cp кодируются непосредственно в одном cu
0x010000 <= cp <= 0x10FFFF
Такие cp занимают 20 бит и хранятся в двух cu которые называются суррогатной
парой. Эти 20 бит бьются на две части по 10 бит и распределяются между cu1 и
cu2:
DC00 <= cu1 <= DFFF (младшая часть)
D800 <= cu2 <= DBFF (старшая часть)
Чуть яснее на картинке:
1101110000000000 0xDCOO
----cu1----
1101111111111111 0xDFFF
+> ------
| 1101100000000000 0xD800
| ----cu2----
| 1101101111111111 0xDBFF
+> ------
|
+--- вот эти части, насколько я понял, это "маркеры" что cu находится в
суррогатной паре и является либо младшим (в случае 110111) либо
старшим (110110).
0xD800 <= cp <= 0xDFFF
Юникод просто резервирует этот рендж для суррогатов в UTF-16.
UTF-8
В Unix-е всегда использовались 8-битные чары и поэтому, чтобы не переписывать весь существующий код на использование 16-битных, придумали UTF-8 - тут code unit занимает восемь бит. Тут нет суррогатных пар, всё немного проще:
First cp Last cp Byte 1 Byte 2 Byte 3 Byte 4
U+0000 U+007F 0xxxxxxx
U+0080 U+07FF 110xxxxx 10xxxxxx
U+0800 U+FFFF 1110xxxx 10xxxxxx 10xxxxxx
U+10000 U+10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
(табличка с википедии)
Т.е. прямо мапим номер кодпоинта в последовательность бит, выбираем столько cu, чтобы хватило места и просто записываем по порядку в свободные биты. Зачем к каждому дополнительному code unit приписывать 10? Чтобы какой-нибудь алгоритм, оказавшись в середине строки, не перепутал дополнительный cu с отдельным символом. Т.е. нам необязательно идти с самого начала строки для того чтобы валидно разбить текст на cp.
У UTF-8 есть ещё одно полезное свойство. Если мы попробуем сравнить две строки лексикографически, то окажется что побайтовое сравнение будет равносильно лексикографическому (но это только в случае если мы используем big endian порядок байт (а можно по другому? я не понял как)).
UTF-32
Тут всё очевидно, но есть недостатки. Есть буквы, которые состоят из нескольких кодпойнтов. Т.е. кодпойнты не имеют ничего общего с символами (в лекции приводят в пример диакритические знаки над буквами, корейское письмо). UTF-32 не панацея для работы над текстом.
Общие проблемы
Вообще очень сложно обобщить все системы письма в мире, поэтому не стоит думать о кодпойнтах как о реальных символах. Поэтому почти нет смысла в том, чтобы считать длину строки, переводить всё в верхний регистр (которого вовсе может не быть) и в других подобных операциях.
Например, некоторые буквы имеют одинаковые кодпойнты, но капитализируются по-разному в зависимости от языка. Ещё есть греческая буква сигма, которая в зависимости от положения в слове имеет разные кодпойнты. А ещё, есть немецкая буква эсцет, которая при капитализации превращается в SS.
Имена файлов
В линуксе именами файлов могут быть любые бинарные данные, главное чтобы не было слеша. Это значит, что для какого-нибудь страшного имени может не существовать валидной строки в любой кодировке. Т.е. с именами файлов в linux нельзя работать как со строками.
В виндоус имена файлов case-insensitive. На стадии форматирования диска винда просто записывает мап из lower-case в upper-case и пользуется им для конверсии имен файлов. И тут как повезёт.