Кодировки

Распространенные заблуждения

Может показаться, что обычного восьмибитного 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 и пользуется им для конверсии имен файлов. И тут как повезёт.