Говоря о тексте, большинство программистов C++ думают о массивах кодов символов и кодировке, которой эти коды соответствуют. Наиболее опытные разработчики вообще не мыслят понятие текста без указания кодировки, наименее опытные просто считают массив байтов с кодами символов данностью и интерпретируют в понятиях кодировки операционной системы. Фундаментальная разница между этими двумя подходами не только в опыте разработчика, но и в том, что не думать о кодировке намного проще. Пора рассмотреть способ, как не заботиться о хранении кодировки, перекодировке текста, получать свободный доступ к символам и при этом видеть безошибочное представление текста вне зависимости от того, кто и где смотрит на строку текста: в Китае ли, в США или на острове Мадагаскар.

8 бит и все-все-все…


Начнем с главного. Создатели языка си были минималистами. По сей день в стандарте C/C++ не предусмотрено типа «байт». Вместо этого типа используется тип char. Char означает character, иными словами — символ. Соответственно, говоря в С/С++ о типе char, мы подразумеваем «байт», и наоборот. Вот тут и начинается самое интересное. Дело в том, что максимально возможное число символов, кодируемых 8 битами, равно 256, и это при том, что на сегодняшний день в таблице Unicode насчитываются сотни тысяч символов.

Хитрые создатели ASCII-кодов сразу же зарезервировали первые 128 кодов под стандартные символы, которыми смело можно закодировать практически все в англоязычном мире, оставив нам лишь половину байта под свои нужды, а точнее лишь один свободный старший бит. В результате в первые годы становления информатики все пытались ужаться в эти оставшиеся «отрицательные» числа от –128 до –1. Каждый набор кодов стандартизировался под определенным именем и с этого момента именовался кодировкой. В какой-то момент кодировок стало больше, чем символов в байте, и все они были несовместимы между собой в той части, что выходила за пределы первых 128 ASCII-символов. В результате, если не угадать с кодировкой, все, что не являет собой набор символов первой необходимости для американского сообщества, будет отображено в виде так называемых кракозябр, символов, как правило, вообще нечитаемых.

Мало того, для одних и тех же алфавитов разные системы вводили кодировки, совершенно рассогласованные между собой, даже если это две системы за авторством одной компании. Так, для кириллицы в MS DOS использовались кодировки 855 и 866, а для Windows уже 1251, все для той же кириллицы в Mac OS используется уже своя кодировка, особняком от них стоят KOI8 и KOI7, есть даже ISO 8859-5, и все будут трактовать одни и те же наборы char совершенно разными символами. Мало того, что было невозможно при обработке различных байт-символов пользоваться сразу несколькими кодировками, например при переводе с русского на немецкий с умлаутами, вдобавок сами символы в некоторых алфавитах ну никак не хотели помещаться в оставленные для них 128 позиций. В результате в интернациональных программах символы могли интерпретироваться в разных кодировках даже в соседних строках, приходилось запоминать, какая строка в какой кодировке, что неизбежно вело к ошибкам отображения текста, от забавных до совсем не смешных.

Поставь себе на виртуальную машину любую другую операционную систему с другой кодировкой по умолчанию, нежели на твоей хостовой системе, например Windows c кодировкой 1251, если у тебя Linux с UTF-8 по умолчанию, и наоборот. Попробуй написать код с выводом строки кириллицей в std::cout, который без изменения кода будет собираться и работать под обеими системами одинаково. Согласись, интернационализация кросс-платформенного кода не такая простая задача.

Пришествие Юникода


Задумка Юникода была проста. Каждому символу раз и навсегда присваивается один код на веки вечные, это стандартизуется в очередной версии спецификации таблицы символов Юникода, и код символа уже не ограничен одним байтом. Великолепная задумка во всем, кроме одного: в языки программирования C/C++ и не только в них символ char раз и навсегда ассоциировался с байтом. Повсюду в коде подразумевался sizeof(char), равный единице. Строки текста же были обычными последовательностями этих самых char, заканчивающимися символом с нулевым кодом. В защиту создателей языка си, Ритчи и Кернигана, следует сказать, что в те далекие 70-е годы никто и подумать не мог, что для кодирования символа понадобится так много кодов, ведь для кодирования символов печатной машинки вполне хватало и байта. Как бы то ни было, основное зло было сотворено, любое изменение типа char привело бы к потере совместимости с уже написанным кодом. Разумным решением стало введение нового типа «широкого символа» wchar_t и дублирование всех стандартных функций языка си для работы с новыми, «широкими» строками. Контейнер стандартной библиотеки C++ string также обрел «широкого» собрата wstring.

Все рады и счастливы, если бы не одно «но»: все уже привыкли писать код на основе байтовых строк, а префикс L перед строковым литералом не добавлял энтузиазма разработчикам на C/C++. Люди предпочитали не использовать символы за пределами ASCII и смириться с ограниченностью латиницы, чем писать непривычные конструкции, несовместимые с уже написанным кодом, работавшим с типом char. Осложняло ситуацию то, что wchar_t не имеет стандартизированного размера: например, в современных GCC-компиляторах g++ он 4 байта, в Visual C++ — 2 байта, а разработчики Android NDK урезали его до одного байта и сделали неотличимым от char. Получилось так себе решение, которое работает далеко не везде. С одной стороны, 4-байтный wchar_t наиболее близок к правде, так как по стандарту один wchar_t должен соответствовать одному символу Юникода, с другой стороны, никто не гарантирует, что будет именно 4 байта в коде, использующем wchar_t.

Альтернативным решением стала однобайтовая кодировка UTF-8, которая мало того, что совместима с ASCII (старший бит, равный нулю, отвечает за однобайтовые символы), так еще и позволяет кодировать вплоть до 4-байтового целого, то есть свыше 2 миллиардов символов. Плата, правда, довольно существенная, символы получаются различного размера, и чтобы, например, заменить латинский символ R на русский символ Я, потребуется полностью перестроить всю строку, что значительно дороже обычной замены кода в случае 4-байтового wchar_t. Таким образом, любая активная работа с символами строки в UTF-8 может поставить крест на идее использовать данную кодировку. Тем не менее кодировка довольно компактно ужимает текст, содержит защиту от ошибок чтения и, главное, интернациональна: любой человек в любой точке мира увидит одни и те же символы из таблицы Юникода, если будет читать строку, закодированную в UTF-8. Конечно, за исключением случая, когда пытается интерпретировать эту строку в другой кодировке, все помнят «кракозябры» при попытке открыть кириллицу в UTF-8 как текст в кодировке по умолчанию в Windows 1251.

Устройство однобайтного Юникода


Устроена кодировка UTF-8 весьма занятно. Вот основные принципы:

  1. Символ кодируется последовательностью байтов, в каждом байте лидирующие биты кодируют позицию байта в последовательности, а для первого байта еще и длину последовательности. Например, так выглядит в UTF-8 символ Я: [1101 0000] [1010 1111]
  2. Байты последовательности, начиная со второго, всегда начинаются с битов 10, соответственно, первый байт последовательности кода каждого символа начинаться с 10 не может. На этом строится основная проверка корректности декодирования кода символа из UTF-8.
  3. Первый байт может быть единственным, тогда лидирующий бит равен 0 и символ соответствует коду ASCII, поскольку для кодирования остается 7 младших бит.
  4. Если символ не ASCII, то первые биты содержат столько единиц, сколько байт в последовательности, включая лидирующий байт, после чего идет 0 как окончание последовательности единиц и потом уже значащие биты первого байта. Как видно из приведенного примера, кодирование символа Я занимает 2 байта, это можно распознать по старшим двум битам первого байта последовательности.
  5. Все значащие биты склеиваются в единую последовательность бит и уже интерпретируются как число. Например, для любого символа, кодируемого двумя байтами, значащие биты я условно помечу символом x: [110x xxxx] [10xx xxxx]

При склейке, как видно, можно получить число, кодируемое 11 битами, то есть вплоть до 0x7FF символа таблицы Юникода. Этого вполне хватает для символов кириллицы, расположенной в пределах от 0x400 до 0x530. При склейке символа Я из примера получится код: 1 0000 10 1111

Как раз 0x42F — код символа Я в таблице символов Юникода.

Другими словами, если не работать с символами в строке, заменяя их другими символами из таблицы Юникода, то можно использовать кодировку UTF-8, она надежна, компактна и совместима с типом char в том плане, что элементы строк совпадают по размеру с байтом, но не обязательно являются при этом символами.

Собственно, именно эффективностью и популярностью кодировки UTF-8 и обусловлено насильственное введение однобайтового wchar_t в Android NDK, разработчики призывают использовать UTF-8, а «широкие» строки не признают как жизнеспособный вид. С другой стороны, Google не так давно отрицал даже исключения в C++, однако весь мир не переспоришь, будь ты хоть трижды Google, и обработку исключений пришлось поддержать. Что касается wchar_t символов с размером в один байт, то множество библиотек уже привыкло к мытарствам с типом wchar_t и дублируют «широкий» функционал обработкой обычных байтовых строк.

UTF (Unicode Transformation Format) — по сути байтовое представление текста, использующее коды символов из таблицы Юникода, запакованные в байтовый массив согласно стандартизированным правилам. Наиболее популярны UTF-8 и UTF-16, которые представляют символы элементами по 8 бит и по 16 бит соответственно. В обоих случаях символ совершенно необязательно занимает ровно 8 или 16 бит, например, в UTF-16 используются суррогатные пары, по сути пары 16-битных значений, используемых вместе. В результате значащих битов становится меньше (20 в случае суррогатной пары), чем битов в группе представляющих символ, но возможности кодировать символы начинают превышать ограничения в 256 или 65 536 значений, и можно закодировать любой символ из таблицы Юникода. Выгодно отличающийся от собратьев UTF-32 менее популярен, ввиду избыточности представления данных, что критично при большом объеме текста.

Пишем по-русски в коде


Беды и дискриминация по языковому признаку начинаются, когда мы пытаемся использовать в коде строку на языке, отличном от ASCII. Так, Visual Studio под Windows создает все файлы в кодировке файловой системы по умолчанию (1251), и при попытке открыть код со строками по-русски в том же Linux с кодировкой по умолчанию UTF-8 получим кучу непонятных символов вместо исходного текста.

Ситуацию частично спасает пересохранение исходников в кодировке UTF-8 с обязательным символом BOM, без него Visual Studio начинает интерпретировать «широкие» строки с кириллицей весьма своеобразно. Однако, указав BOM (Byte Order Mark — метка порядка байтов) кодировки UTF-8 — символ, кодируемый тремя байтами 0xEF, 0xBB и 0xBF, мы получаем узнавание кодировки UTF-8 в любой системе.

BOM — стандартный заголовочный набор байтов, нужный для распознавания кодировке текста в Юникоде, для каждой из кодировок UTF он выглядит по-разному. Не стесняйся использовать родной язык в программе. Даже если тебе придется локализовывать ее в другие страны, механизмы интернационализации помогут превратить любую строку на одном языке в любую строку на другом. Разумеется, это в случае, если продукт разрабатывается в русскоязычном сегменте.

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

Писать константы и переменные, а также названия функций кириллицей все же не нужно. Одно дело — выводить константы на родном языке, совсем другое — писать код, постоянно переключая раскладку. Не самый лучший вариант.

Различаем тип «байты» и тип «текст»


Главное, что нужно уметь и иметь в виду, — что тип «текст» в корне отличается от типа «набор байтов». Если мы говорим о строке сообщения, то это текст, а если о текстовом файле в некоторой кодировке, то это набор байтов, который можно вычитать как текст. Если по сети нам приходят текстовые данные, то они приходят к нам именно байтами, вместе с указанием кодировки, как из этих байтов получить текст.

Если посмотреть на Python 3 в сравнении с Python 2, то третья версия совершила по-настоящему серьезный скачок в развитии, разделив эти два понятия. Крайне рекомендую даже опытному C/C++ разработчику поработать немного в Python 3, чтобы ощутить всю глубину, с которой произошло разделение текста и байтов на уровне языка в Python. Фактически текст в Python 3 отделен от понятия кодировки, что для разработчика C/C++ звучит крайне непривычно, строки в Python 3 отображаются одинаково в любой точке мира, и если мы хотим работать с представлением этой строки в какой-либо кодировке, то придется преобразовать текст в набор байтов, с указанием кодировки. При этом внутреннее представление объекта типа str, по сути, не так важно, как понимание, что внутреннее представление сохранено в Юникоде и готово к преобразованию в любую кодировку, но уже в виде набора байтов типа bytes.

В C/C++ подобный механизм нам мешает ввести отсутствие такой роскоши, как потеря обратной совместимости, которую позволил себе Python 3 относительно второй версии. Одно лишь разделение типа char на аналог wchar_t и byte в одной из следующих редакций стандарта приведет к коллапсу языка и потере совместимости с непомерным количеством уже написанного кода на С/С++. Точнее, всего, на чем ты сейчас работаешь.

Веселые перекодировки


Итак, исходная проблема осталась нерешенной. У нас по-прежнему есть однобайтовые кодировки, как UTF-8, так и старые и недобрые однобайтовые кодировки вроде кодировки Windows 1251. С другой стороны, мы задаем строковые константы широкими строками и обрабатываем текст через wchar_t — «широкие» символы.

Здесь нам на помощь придет механизм перекодировок. Ведь, зная кодировку набора байтов, мы всегда сможем преобразовать его в набор символов wchar_t и обратно. Не спеши только самостоятельно создавать свою библиотеку перекодировки, я понимаю, что коды символов любой кодировки сейчас можно найти за минуту, как и всю таблицу кодов Юникода последней редакции. Однако библиотек перекодировки достаточно и без этого. Есть кросс-платформенная библиотека libiconv, под лицензией LGPL, самая популярная на сегодняшний день для кросс-платформенной разработки. Перекодировка сводится к нескольким инструкциям:

iconv_t conv = iconv_open("UTF-8","CP1251");
iconv(conv, &src_ptr, &src_len, &dst_ptr, &dst_len);
iconv_close(conv);

Соответственно, сначала создание обработчика перекодировки из одной кодировки в другую, затем сама операция перекодировки одного набора байтов в другой (даже если один из наборов байтов на самом деле байты массива wchar_t), после чего обязательное закрытие созданного обработчика перекодировки. Есть также и более амбициозная библиотека ICU, которая предоставляет как C++ интерфейс для работы с перекодировкой, так и специальный тип icu::UnicodeString для хранения непосредственно текста в представлении Юникода. Библиотека ICU также является кросс-платформенной, и вариантов ее использования предоставляется на порядок больше. Приятно, что библиотека сама заботится о создании, кешировании и применении обработчиков для перекодировки, если использовать C++ API библиотеки.

Например, чтобы создать строку в Юникоде, предлагается использовать обычный конструктор класса icu::UnicodeString:

icu::UnicodeString text(source_bytes, source_encoding);

Таким образом, предлагается полностью отказаться от типа wchar_t. Проблема, однако, в том, что внутреннее представление Юникода для такой строки установлено в два байта, что влечет за собой проблему в случае, когда код за эти два байта выходит. Кроме того, интерфейс icu::UnicodeString полностью несовместим со стандартным wstring, однако использование ICU — хороший вариант для С++ разработчика.

Кроме того, есть пара стандартных функций mbstowcs и wcstombs. В общем и целом при правильно заданной локали они, соответственно, преобразуют (мульти-) байтовую строку в «широкую» и наоборот. Расшифровываются сокращения mbs и wcs как Multi-Byte String и Wide Character String соответственно. Кстати, большинство привычных разработчику на языке си функций работы со строками дублируются именно функциями, в которых в названии str заменено на wcs, например wcslen вместо strlen или wcscpy вместо strcpy.

Нельзя не вспомнить и о Windows-разработке. Счастливых обладателей WinAPI ждет очередная пара функций с кучей параметров: WideCharToMultiByte и MultiByteToWideChar. Делают эти функции ровно то, что говорят их названия. Указываем кодировку, параметры входного и выходного массива и флаги и получаем результат. Несмотря на то что функции эти внешне страшненькие, работу свою делают быстро и эффективно. Правда, не всегда точно: могут попытаться преобразовать символ в похожий, поэтому осторожнее с флагами, которые передаются вторым параметром в функцию, лучше указать WC_NO_BEST_FIT_CHARS.

Пример использования:

WideCharToMultiByte( CP_UTF8,
    WC_NO_BEST_FIT_CHARS,
    pszWideSource, nWideLength,
    pszByteSource, nByteLength,
    NULL, NULL );

Разумеется, этот код не переносим на любую платформу, кроме Windows, поэтому крайне рекомендую пользоваться кросс-платформенными библиотеками ICU4C или libiconv.

Наиболее популярная библиотека — именно libiconv, однако в ней используются исключительно параметры char*. Это не должно пугать, в любом случае массив чисел любой битности — это всего лишь набор байтов. Следует, однако, помнить про направление двубайтовых и более чисел. То есть в каком порядке в байтовом массиве представлены байты — компоненты числа. Различают Big-endian и Little-endian соответственно. Общепринятый порядок представления чисел в подавляющем большинстве машин — Little-endian: сначала идет младший байт, а в конце старший байт числа. Big-endian знаком тем, кто работает с протоколами передачи данных по сети, где числа принято передавать начиная со старшего байта (часто содержащего служебную информацию) и кончая младшим. Следует быть аккуратным и помнить, что UTF-16, UTF-16BE и UTF-16LE — не одно и то же.

Класс текста


Давай теперь аккумулируем полученные знания и решим исходную задачу: нам нужно создать сущность, по сути класс, инициализируемый строкой, либо «широкой», либо байтовой, с указанием кодировки, и предоставляющий интерфейс привычного контейнера строки std::string, с возможностью обращения к элементам-символам, изменяя их, удаляя, преобразуя экземпляр текста в строке как «широкой», так и байтовой с указанием кодировки. В общем, нам нужно значительно упростить работу с Юникодом, с одной стороны, и получить совместимость с прежде написанным кодом, с другой стороны.

Класс текста, таким образом, получит следующие конструкторы:

text(char const* byte_string, char const* encoding);
text(wchar_t const* wide_string);

Стоит перегрузить также от std::string и std::wstring вариантов, а также от итераторов начала и конца контейнера-источника.

Доступ к элементу, очевидно, должен быть открыт, но в качестве результата нельзя использовать байтовый char или платформозависимый wchar_t, мы должны использовать абстракцию над целочисленным кодом в таблице Юникода: symbol.

symbol& operator [] (int index);
symbol const& operator [] (int index) const;

Таким образом, становится очевидно, что мы не можем сохранять строку Юникода в виде char или wchar_t строки. Нам нужно как минимум std::basic_string<int32_t>, поскольку на данный момент кодировки UTF-8 и UTF-16 кодируют символы в пределах int32_t, не говоря про UTF-32.

С другой стороны, за пределами класса text никому не нужен наш `std::basic_string<int32_t>`, назовем его `unicode_string`. Все библиотеки любят работать с `std::string` и `std::wstring` или `char const*`` и `wchar_t const*`. Таким образом, лучше всего кешировать как входящий std::string или std::wstring, так и результат перекодировки текста в кодировку байт-строки. Мало того, часто наш класс text понадобится лишь как временное хранилище для путешествующей строки, например байтовой в UTF-8 из базы данных в JSON-строку, то есть перекодирование в unicode_string нам понадобится лишь по требованию обратиться к элементам — символам текста. Текст и его внутреннее представление — это тот класс, который должен быть оптимизирован по максимуму, так как предполагает интенсивное использование, а также не допускать перекодировок без причины и до первого требования. Пользователь API класса text должен явно указать, что хочет преобразовать текст в байтовую строку в определенной кодировке либо получить специфичную для системы «широкую» строку:

std::string const& byte_string(std::string const& encoding) const;
std::wstring const& wide_string() const;

Как видно выше, мы возвращаем ссылку на строку, которую мы высчитали и сохранили в поле класса. Разумеется, нам нужно будет почистить кеш с `std::string` и `std::wstring` при первом же изменении значения хотя бы одного символа, здесь нам поможет operator -> от неконстантного this класса данных `text::data`. Как это делать, смотри предыдущие два урока академии C++.

Нужно не забыть также и о получении char const* и wchar_t const*, что несложно делается, учитывая то, что std::string и std::wstring кешируются полями класса text.

char const* byte_c_str(char const* encoding) const;
wchar_t const* wide_c_str() const;

Реализация сводится к вызову `c_str()`` у результатов byte_string и wide_string соответственно.

Можно считать кодировкой по умолчанию для байтовых строк UTF-8, это гораздо лучше, чем пытаться работать с системной кодировкой по умолчанию, так код в зависимости от системы будет работать по-разному. Введя ряд дополнительных перегрузок без указания кодировки при работе с байтовыми строками, мы также получаем возможность переопределить оператор присвоения:

text& operator = (std::string const& byte_string); // в кодировке ”UTF-8”
text& operator = (std::wstring const& wide_string);

Нужно также не забыть о перегрузке операторов + и +=, но в целом остальные операции можно уже сводить к аргументу и результату типа text, универсальному значению, предоставляющему текст вне зависимости от кодировки.

Разумеется, Академия C++ не была бы академией, если бы я не предложил тебе теперь реализовать класс текста самостоятельно. Попробуй создать класс text на основе материала этой статьи. Реализация должна удовлетворять двум простым свойствам:

  • Классом должно быть удобнее пользоваться, чем стандартными строками, вдобавок класс предоставляет совместимость либо взаимное преобразование с типами `std::string`, `std::wstring`, `char const*`` и `wchar_t const*`.
  • Класс подразумевает максимальную оптимизацию, работа со строками не должна быть дороже, чем при работе со стандартными std::string и std::wstring. То есть никаких неявных перекодировок, пока API явно не подразумевает перекодировку содержимого, иначе классом никто не будет пользоваться.

Здесь как раз имеет смысл обработать дополнительно неконстантный `operator ->`` для сброса кеша со строками, однако оставляю это на усмотрение разработчика. То есть тебя. Удачи!

Разумеется, в реализации не обойтись без класса `copy_on_write` из предыдущих статей. Как обычно, на всякий случай напоминаю его упрощенный вид:

template <class data_type>
class copy_on_write
{
public:
   copy_on_write(data_type* data)
       : m_data(data) {
    }
   data_type const* operator -> () const {
       return m_data.get();
    }
   data_type* operator -> () {
       if (!m_data.unique())
           m_data.reset(new data_type(*m_data));
       return m_data.get();
    }
private:
   std::shared_ptr<data_type> m_data;
};

Что мы получаем


Реализовав класс text, мы получим абстракцию от множества кодировок, все, что нам потребуется, — одна перегрузка от класса text. Например, так:

text to_json() const;
void from_json(text const& source);

Нам больше не нужно множество перегрузок от `std::string` и `std::wstring`, не нужно будет переходить на поддержку «широких» строк, достаточно заменить в API ссылки на строки на text, и получаем Юникод автоматом. Вдобавок мы получаем отличное кросс-платформенное поведение, вне зависимости от того, какую библиотеку мы выбрали в качестве движка перекодировки, — ICU4C или libiconv, ввиду того, что внутреннее представление у нас всегда UTF-32 при распаковке символов и мы нигде не завязаны на платформозависимый wchar_t.

Итого: у нас есть совместимость либо взаимоконвертация со стандартными типами, а значит, и упрощение поддержки Юникода на стороне кода, написанного на С++. Ведь если мы пишем высокоуровневую логику на C++, меньше всего нам хочется получить проблемы при использовании wchar_t символов и кучи однообразного кода при обработке и перекодировке текста.

При том что сама перекодировка уже реализована в тех же ICU4C и libiconv, алгоритм для внутренней работы класса text довольно прост. Дерзай, и, может, уже завтра именно твоя библиотека работы с текстом будет использоваться повсюду в качестве высокоуровневой абстракции при обработке любых текстовых данных, от простого JSON с клиента до сложных текстовых структур со стороны различных баз данных.

image

Впервые опубликовано в журнале Хакер #191.
Автор: Владимир Qualab Керимов, ведущий С++ разработчик компании Parallels


Подпишись на «Хакер»

Комментарии (68)


  1. Gorthauer87
    20.05.2015 11:53
    +4

    Как-то мне кажется, что авторы описывают QString. Как по мне, именно Qtшные строки самые удобные и простые. Хотя насчет скорости не уверен.


    1. ololoepepe
      20.05.2015 13:59

      QString stores a string of 16-bit QChars, where each QChar corresponds one Unicode 4.0 character. (Unicode characters with code values above 65535 are stored using surrogate pairs, i.e., two consecutive QChars.)

      Для подмножества символов с кодом меньше 65535 все должно быть неплохо. Плюс следующее:
      Behind the scenes, QString uses implicit sharing (copy-on-write) to reduce memory usage and to avoid the needless copying of data. This also helps reduce the inherent overhead of storing 16-bit characters instead of 8-bit characters.

      То есть и потребление памяти разумное.
      Для задач, где Qt применим, скорости QString более чем достаточно. Если же требуется феноменальная производительность, то вряд ли вообще допустимо будет использовать что-то кроме голого char и собственного велосипедного кода.


      1. agmt
        20.05.2015 19:12
        +5

        Как по мне, так лучше уж utf-8, который явно говорит «символ занимает неизвестное количество байт», чем QString, провоцирующий на редкие ошибки иллюзией, что любой символ — это 1 элемент массива и наоборот.


        1. Qualab
          26.05.2015 22:31

          Если взять QString с внутренним представлением int32_t на символ с ленивым преобразованием из входящей закодированной строки, с кэшем до первого не-const вызова, то вполне себе подойдёт. В QString много чего ещё стоило бы подкрутить.


  1. Cheater
    20.05.2015 12:30
    +9

    > Ситуацию частично спасает пересохранение исходников в кодировке UTF-8 с обязательным символом BOM, без него Visual Studio начинает интерпретировать «широкие» строки с кириллицей весьма своеобразно.

    Использовать Byte Order Mark? Серьёзно? Этот костыль времён CP1251 просто не имеет права на существование. Равно как и IDE/системы сборки, которым, видите ли, нельзя сказать «воспринимайте это как UTF-8».


    1. ololoepepe
      20.05.2015 14:01
      +3

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


    1. agmt
      20.05.2015 19:10
      -3

      BOM кодирует невозможную последовательность в данной кодировке, так что для адекватного редактора он не должен составлять проблем. Хотя какой-то старый gcc очень ругался на него, но это уже косяк gcc.


      1. ololoepepe
        20.05.2015 19:44
        +2

        Конечно, раз костыль не мешает, давайте его везде пихать, ведь так сложно прикрутить libenca.


    1. Qualab
      26.05.2015 22:38

      Распознать UTF-8 без BOM особого труда не составит, но вот проблема с короткими текстовыми файлами в этом случае останется, не так уж мало символов, начинающихся с 110 и 10. В этом смысле BOM гарантирует нам то, что текст закодирован в Unicode нужной кодировки. Для каждой кодировки Unicode свой BOM, описанный в стандарте. Костылями его считать можно только в случае если мы сидим исключительно под *nix в кодировке UTF-8 и считаем всё подряд UTF-8. Ребята из разработки Android NDK вообще не стесняются wstring считать строкой исключительно в UTF-8. Фактически кодировок больше чем одна и никакое распознавание не заменит простой заголовок текстового файла в несколько символов (с которым банально не умеет работать половина OpenSource софта, включая, до недавнего времени, Eclipse). Код должен быть кроссплатформенным и стабильным к косякам кривой перекодировки, которые вы будете неизбежно получать, пытаясь кодировку угадать.


  1. risik
    20.05.2015 12:41
    +1

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

    > придется локализовывать ее в другие страны, механизмы интернационализации

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


  1. Googolplex
    20.05.2015 12:46
    +1

    Вот это следует прочитать всем: utf8everywhere.org. Отлично расписано, почему нет никаких причин использовать что-то, кроме UTF-8.

    Насчёт питона, кстати, тоже не всё так однозначно: lucumr.pocoo.org/2014/1/5/unicode-in-2-and-3, lucumr.pocoo.org/2014/5/12/everything-about-unicode. Я считаю, что разделение строк на юникодные и байтовые абсолютно необходимо, но, судя по всему, в Python 3 это сделано, по меньшей мере, странно.

    На мой взгляд, удобнее всего строки реализованы в Go и Rust (особенно в последнем). В них строка — это просто набор байт, обозначающий текст в UTF-8. Всё. Эти языки так же обеспечивают гарантии того, что все строки являются валидными UTF-8-последовательностями.


    1. Gorthauer87
      20.05.2015 13:30
      +1

      Но всё-таки проблемы с заменой символов таки отрицательно влияют на производительность: ведь если есть символ b, а нужно его заменить на Б, то начнётся веселая чехарда: придется или в лучшем случае все символы за b сдвигать на разницу в длине или же вообще заново выделять память. То есть внутреннее представление может и отличаться, если возникает необходимость в частой обработке подстрок. А если такой необходимости нет, то и банального char * хватит за глаза и за уши.


      1. Googolplex
        20.05.2015 13:50
        +4

        проблемы с заменой символов

        Определите сначала, что такое «символ». Есть как минимум 2 варианта — code point и grapheme cluster. Если вы пишете, скажем, текстовый редактор, то вам нужно работать именно с кластерами, и там в любом случае не обойдётся заменой одного байта на другой, вне зависимости от кодировки строк. Работать с code point'ами же в плане замены очень опасно — например, очень легко сломать диакритику.

        Кроме того, настоящая кодировка с фиксированной длиной code unit'а — это UTF-32. UTF-16 с двух- и четырёх-байтовыми code unit'ами имеет совершенно те же проблемы, что и UTF-8, в плане замены code point'ов. Если нужно часто осуществлять такие операции, то строку в любом случае нужно сконвертировать в UTF-32. Но в качестве основного формата хранения строк UTF-32 не пригоден, потому что у него огромный оверхед по памяти.


        1. Googolplex
          20.05.2015 13:58

          Вот, кстати, очень хороший пример, когда посимвольная замена не работает в любом случае, вне зависимости от кодировки строк (цитата с utf8everywhere):

          toupper() and tolower() shall not be phrased in terms of code units, as it does not work in Unicode. For example, the Latin ? ligature must be converted to FFL and the German ? to SS (there is a capital form ?, but the casing rules follow the traditional ones).


        1. Qualab
          26.05.2015 22:40

          Символ — это единичный элемент текста. В отрыве от кодировки. Сам по себе символ — это логическая сущность из мира человеческой логики, к машинным 0 и 1 не имеющая никакого отношения. Конечно же вас путает ключевое слово char, языков C/C++.


          1. Googolplex
            27.05.2015 12:54

            Я не знаю, почему вы решили, что меня, что-то путает. Именно это я и имел в виду — абстрактный символ как понятие отделено от кодировки, а из-за того, что в общем случае преобразования символов не имеют вида «один к одному» (как, например, toupper(?)=SS), завязываться на то, что в программе каждый символ закодирован фиксированным количеством байт — глупо и неправильно.


            1. Qualab
              29.05.2015 19:13

              Код символа целое число, в C/C++ есть достаточно вместимый тип для любого кода Юникод из возможных — это int32_t, так что если нужно соответствие 1:1, то его легко можно получить.


              1. Googolplex
                29.05.2015 19:20

                Вы понимаете, что ? — это один code point, а SS (его toupper-версия) — это два? Здесь в принципе невозможно получить соответствие 1:1 для code point'ов, вне зависимости от используемых типов.


                1. Qualab
                  29.05.2015 19:34

                  Я вижу два символа, а вот toupper как раз не показатель. Всякие надстрочные и подстрочные символы — суть отдельные символы. Если определить соответствие 1:1 как соответствие одного символа одному коду таблицы Юникод, то всё становится на свои места.


                  1. Googolplex
                    29.05.2015 19:42
                    +2

                    Я не понимаю, к чему вы ведёте. Напомню, с чего началась дискуссия. Gorthauer87 написал:

                    ведь если есть символ b, а нужно его заменить на Б, то начнётся веселая чехарда: придется или в лучшем случае все символы за b сдвигать на разницу в длине или же вообще заново выделять память


                    Я ответил, что в общем случае замена code point'ов один к одному не работает, даже простейшие операции вроде toupper() приводят к изменению общего количества code point'ов.

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


                    1. Qualab
                      03.06.2015 21:40

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


                      1. ilammy
                        04.06.2015 23:12

                        Используйте UTF-8. Одним из её замечательных свойств является как раз корректная работа стандартных алгоритмов поиска подстроки в байтовых массивах: они никогда не найдут подстроку, которая начинается посредине символа (при условии корректных строк на входе).

                        Если же вам необходимы особые подстроки (типа, без учёта регистра или диакритики), то вам в любом случае потребуется языкозависимый парсер и специальная собиралка строк, а не просто str[i] = subst[j].


                        1. Qualab
                          05.06.2015 17:42

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


                          1. ilammy
                            05.06.2015 17:59

                            Зачем склеивать? Если вам надо найти подстроку "©?" в UTF-8 строке, то вы ищете последовательность «0xC2 0xA9 0xE2 0x98 0x83», как если бы искали подстроку в однобайтовой кодировке.


                            1. Qualab
                              05.06.2015 19:39

                              Не всё так просто, это в простейшем случае regex банально ищет подстроку в строке, чаще требуется находить группы или множества символов, производить замену групп и множеств в различных комбинациях. Regex логически работает с символами, а не с цепочками байт в строке.


      1. Googolplex
        20.05.2015 13:55
        +3

        банального char * хватит за глаза и за уши

        Строго говоря, именно об этом говорится на utf8everywhere — следует использовать char *-строки как хранилище UTF-8, и конвертировать в другие кодировки на границах с другими API (что в основном актуально только для винды, потому что в Unix-системах системные вызовы работают с байтами, и им наплевать на кодировки).


        1. Qualab
          26.05.2015 22:41

          Угу, очень удобно бегать по символам, при обходе regexp'ом. А уж индексация какая удобная! Константная сложность резко становится линейной.


          1. Googolplex
            27.05.2015 12:58

            А уж индексация какая удобная!

            Уже много раз было сказано — «посимвольная» индексация строк, что бы в данном контексте «символ» не обозначал, — это совершенно бессмысленное действие. В 99% случаев строки в программах никогда не индексируются. Там же, где они индексируются, в 99% случаев это должна быть «семантическая» индексация, на уровне графемных кластеров, а в этом случае вы в принципе не сможете обеспечить индексацию за постоянное время, потому что графемные кластеры могут быть произвольной длины, безотносительно используемых кодировок.

            Для всех оставшихся случаев (когда вам зачем-то нужна быстрая индексация по code point'ам) — есть UTF-32.


            1. Qualab
              29.05.2015 19:15

              Хорошо, даже если оставить за бортом индексацию (которая вообще-то крайне полезна, если мы работаем с текстом как с массивом символов), то что же делать с regex? Можете сто раз сказать «графемные кластеры», легче от этого не станет.


              1. Googolplex
                29.05.2015 19:30
                +1

                если мы работаем с текстом как с массивом символов

                Именно про это я и написал в комментарии, на который вы ответили — такая работа с текстом очень редка, в частности, потому, что в случае юникода «массив символов» в смысле «массив code point'ов» бесполезен практически для любого практического применения.

                По поводу регулярок.

                В общем случае нужно определиться, с чем работают регулярные выражения. Как правило, они работают над code point'ами (корректность такого подхода в общем случае можно оспорить, ну да ладно). В таком случае есть два варианта — либо переводить строки в UTF-32 внутри движка и работать с фиксированной длиной encoded character'а, либо нужно просто построить таблицу соответствия «номер encoded character'а -> смещение от начала строки в code unit'ах» и работать с ним, что тоже даст O(1) доступ по номеру code point'а в UTF-8.

                Кстати, второй вариант отлично подходит для регулярок на графемных кластерах — просто будет номер кластера а не encoded character'а.


                1. Qualab
                  29.05.2015 19:35

                  Да вы заколебётесь перестраивать хэш-таблицу с индексами индексов(!) при любой регулярке, изменяющей текст.


                  1. Googolplex
                    29.05.2015 19:44

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


                    1. Qualab
                      03.06.2015 21:42
                      -1

                      Да ладно, каждый раз у вас будет перестроение таблицы символов. Хэш-таблица весьма чутко реагирует на любое мало-мальски серьёзное перестроение и чтобы оставить всё с константным доступом перестраивает весь исходный набор цепочек коллизий. Вы хотите после каждого регэкспа перестроения хэш-таблицы? Весьма спорное стремление.


                      1. Googolplex
                        04.06.2015 11:12

                        Да от чего будет перестроение таблицы-то? Если регэкспы не меняют исходную строку? (не говоря уже о том, что хеш-таблица здесь — не лучшее решение, массив индексов проще и удобнее будет)


                        1. Qualab
                          05.06.2015 17:45

                          Массив индексов будет перестраиваться каждый раз, когда будет применяться операция замены по регулярному выражению. По сути может перестроиться вся строка, а фактически и весь набор твоих элементов. Либо ограничиваешься константностью представленного строкового объекта, вынуждая разработчика автоматически и безконтрольно создавать совершенно ненужные промежуточные мини-объекты строк, как это сделано, например, в Python. Если же у тебя два массива дублируют друг друга, возникает также проблема консистентности двух представлений одних и тех же данных.


    1. sebres
      20.05.2015 14:48

      Соглашусь частично, но:

      • utf-8 иногда довольно медленный. Например, я не видел ни одного regexp-движка работающего быстро напрямую на utf-8 последовательностях, просто потому что вот такое вот ".{1,10}" — очень нехорошее под-выражение на utf-8, а если non-gready (т.е. ".{1,10}?"), то все еще хуже. Т.е. или пре- а затем пост-конвертор в/из multi-byte фиксированной длинны (типа unicode) туда-обратно, что на больших строках совсем не есть гуд, или падение скорости сразу, как минимум на multibyte символах
      • тоже можно практически всегда сказать про обработку binary в utf-8 представлении, буде это по какой-либо причине необходимо или в силу обстоятельств так случилось;
      • нужно не забывать про данные снаружи, начиная от пользовательского ввода — к примеру «неправильно» отэскэпленый урл параметер (или тупо в single-byte системной кодировке клиента) и заканчивая внешними базами или конфигами, например binary данные, словари в single-byte кодировке и т.д.

      Т.е. лучше иметь возможность двойного внутреннего представления (utf-8 и unicode/binary/enc-X), с авто конвертированием в/из utf-8 при необходимости, как это сделано например в tcl.

      Про третий питон отчасти согласен тоже, но часто это только если делаешь конвертацию «вручную» или если имеем «неправильные» для конкретной кодировки символы. Имхо это как раз из-за того, что 2-й питон кое-что не умел или вернее делал это не совсем правильно. Типичный пример «борьбы» с «help, my umlauts are gone» в 3-м питоне можно подглядеть в одном моем баг-фиксе. И что самое противное, я почти не сомневаюсь, что появится "if (ver > 4)" или что имхо много хуже "if (ver > 3.5)".

      На тикле же например — это все либо совсем автоматически, либо в исключительных случаях в одну строчку «encoding convertfrom» или обратно «encoding convertto». И по причине, того что в tcl это чуть не с самого рождения и он всегда донельзя обратно совместим, т.е. никаких if-version-since-whatever…


      1. Googolplex
        20.05.2015 15:01

        Да, как мне кажется, движки регулярных выражений — это одно из исключений в плане выбора кодировки. Если предполагается обрабатывать юникодные строки, то такие движки должны работать в UTF-32.

        Что такое обработка binary в UTF-8-представлении? о_О

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

        Насчёт нужности автоматического преобразования между внутренними представлениями строки не уверен, но это не так важно — главное, чтобы всегда было известно, где какая кодировка. В Rust, например, вы всегда можете преобразовать String в Vec<u8> (получить сырые байты строки, обозначающие символы в UTF-8) и в Vec<char> (получить вектор 32-битных code unit'ов, непосредственно соответствующих code point'ам), причём первое преобразование «бесплатно», второе же требует аллокации и преобразования кодировки.

        Кстати, что вы подразумеваете под unicode-представлением? Unicode — это просто таблица символов, это не кодировка. Тот же вопрос про binary.


        1. sebres
          20.05.2015 18:16

          Что такое обработка binary в UTF-8-представлении? о_О
          Можно пример я на тикле напишу (запускать в tclsh, или если в виндовс то в wish-консоли (т.к. мултибайт)).

          Навскидку не могу придумать наглядный прямой пример, поэтому пример чуть-чуть обратный — есть несколько бинарных кусков (массивов), ищем в них немецкие умляуты, причем важно, из-за скорости, не конвертировать бинарный массив при поиске (т.е. оставляем бинарную строчку как есть — переменная bin на входе hasGermanUml). Т.е. конвертирование которое вы видите перед вызовом hasGermanUml — просто чтобы получить бинарник для входа (т.е. для наглядности).
          Пример под спойлером
          proc bin2hex {bin} {
            binary scan $bin H* hex
            return $hex
          } 
          
          proc hasGermanUml {bin} {
            set map [list A O U]
            set map [concat $map [string tolower $map]]
            set map [concat $map [encoding convertto utf-8 $map]]
            puts "Ищем $map в $bin ([bin2hex $bin]) ..."
            foreach c $map {
              if { [string first $c $bin] != -1 } {
                return "Yes! may be german text, contains german umlaut '$c' ([bin2hex $c])"
              }
            }
            regexp {[^a-zA-Z]+} $bin c
            return "No! probably not a german text, does not contains german umlauts, found char '$c' ([bin2hex $c])"
          }
          
          ## ----------
          
          set txt [encoding convertfrom cp1252 Schr[set c \xf6]der]
          
          set utfAsBin [encoding convertto utf-8 $txt]
          set utfAsBin2 [encoding convertfrom utf-8 $utfAsBin]
          
          puts "\nБинарный массив содержащий utf-8 '$utfAsBin' (как-бы двойная кодировка) ..."
          puts [hasGermanUml $utfAsBin]
          puts "\nИли это правда символы cp1252 '$utfAsBin2' но напрямую utf-8 ..."
          puts [hasGermanUml $utfAsBin2]
          
          set utfAsBin2 [encoding convertfrom cp1251 $txt]
          set utfAsBin [encoding convertto utf-8 $utfAsBin2]
          
          puts "\nБинарный массив содержащий utf-8 '$utfAsBin' (как-бы двойная кодировка, тот же символ, но из cp1251, не должен найти) ..."
          puts [hasGermanUml $utfAsBin]
          puts "\nUtf-8 (тот же символ, но из cp1251) '$utfAsBin2' напрямую utf-8 ... (не должен найти) ..."
          puts [hasGermanUml $utfAsBin2]
          

          Результат ниже (некоторые символы съел хабрапарсер или браузер):

          Бинарный массив содержащий utf-8 'SchrA¶der' (как-бы двойная кодировка) ...
          Ищем A O U a o u A? A? A? A¤ A¶ A? в SchrA¶der (53636872c3b6646572) ...
          Yes! may be german text, contains german umlaut 'A¶' (c3b6)
          
          Или это правда символы cp1252 'Schroder' но напрямую utf-8 ...
          Ищем A O U a o u A? A? A? A¤ A¶ A? в Schroder (53636872f6646572) ...
          Yes! may be german text, contains german umlaut 'o' (f6)
          
          Бинарный массив содержащий utf-8 'SchrN?der' (как-бы двойная кодировка, тот же символ, но из cp1251, не должен найти) ...
          Ищем A O U a o u A? A? A? A¤ A¶ A? в SchrN?der (53636872d186646572) ...
          No! probably not a german text, does not contains german umlauts, found char 'N?' (d186)
          
          Utf-8 (тот же символ, но из cp1251) 'Schrцder' напрямую utf-8 ... (не должен найти) ...
          Ищем A O U a o u A? A? A? A¤ A¶ A? в Schrцder (5363687246646572) ...
          No! probably not a german text, does not contains german umlauts, found char 'ц' (46)
          


          И чтобы было понятней:

          puts [set c1 [encoding convertfrom cp1251 \xf6]]; # utf-8 "ц"
          puts [set c2 [encoding convertfrom cp1252 \xf6]]; # utf-8 "o"
          puts -[encoding convertto utf-8 $c1]-; # "ц" как бы в binary (utf-8 byte array).
          puts -[encoding convertto utf-8 $c2]-; # "o" как бы в binary (utf-8 byte array).
          puts -[encoding convertto unicode $c1]-; # "ц" как бы в binary (unicode byte array).
          puts -[encoding convertto unicode $c2]-; # "o" как бы в binary (unicode byte array).
          


          1. Googolplex
            20.05.2015 19:32
            +2

            Это если бы я был студентом, а вы профессором

            Прошу прощения, но это абсолютно необоснованное утверждение. Именно из-за того, что программисты путают юникод и его кодировки, возникают опасные представления о том, как с ним правильно работать. Unicode — это таблица символов, ничего больше. Представление code point'ов юникода в виде последовательность байт (т.е. кодировка) — это один из Unicode Transformation Format'ов, т.е. UTF'ов. Пытаться называть их как-то ещё, даже если их так называют в любимом языке — это путь к неправильному пониманию того, как работает юникод, и к ошибкам в программе.

            Вот цитата из документации Tcl:
            Strings in Tcl are logically a sequence of 16-bit Unicode characters. These strings are represented in memory as a sequence of bytes that may be in one of several encodings: modified UTF-8 (which uses 1 to 3 bytes per character), 16-bit “Unicode” (which uses 2 bytes per character, with an endianness that is dependent on the host architecture), and binary (which uses a single byte per character but only handles a restricted range of characters). Tcl does not guarantee to always use the same encoding for the same string.

            Из этого следует, что внутреннее представление строк в тикле ущербно — как и в Java, например. Если оставить в стороне некорректное использование терминологии Unicode, получается, что естественной кодировкой для строк в тикле является UCS-2 (потому что именно UCS-2 позиционировалась, как fixed-byte-кодировка с 16-битными «символами»). Два остальных представления (modified UTF-8 и binary, которая, похоже, ASCII) строго совместимы с UCS-2. В частности, из-за в тикле не получится естественным образом представить code point'ы юникода извне BMP — такие строки, скорее всего, будут изображать UTF-16 состоять из двух «символов» (как в Java).

            Ваш первый пример — наполовину читерский, наполовину удачливый. Там нет никакого binary в UTF-8 представлении. Там имеется следующее.

            Во-первых, строки в тикле действительно могут менять внутреннее представление и содержать «символы» переменной длины. Здесь важно то, что convertfrom обрабатывает только первый байт каждого «символа». Более того, строки в тикле используются для хранения как символьных данных, так и бинарных (что, с учётом их внутренного представления, по меньшей мере недальновидно).

            Во-вторых, переменные у вас имеют следующий смысл:

            txt — строка во внутреннем представлении, полученная декодированием последовательности байт в кодировке cp1252

            utfAsBin — последовательность байт в кодировке UTF-8, полученная кодированием txt
            utfAsBin2 — строка во внутреннем представлении, полученная декодированием последовательности байт в кодировке UTF-8

            utfAsBin2 — строка во внутреннем представлении, полученная декодированием txt как последовательности байт в кодировке cp1251 (из-за того, что convertfrom рассматривает только нижние байты каждого символа, а в UCS-2 символы с кодами 128-255 совпадают с Latin-1, с которой cp1252 наполовину бинарно совместима, всё проходит замечательно — символ o распознаётся как ц и декодируется в ц)
            utfAsBin — последовательность байт в кодировке UTF-8, полученная кодированием utfAsBin2

            Если внимательно следить за кодировками и преобразованиями, то всё остаётся довольно логичным, но из-за особого внутреннего представления строк и того, что одни и те же строки используются для хранения и символьных, и двоичных данных, когнитивная нагрузка сильно возрастает.

            В-третьих, читерство заключается в том, как вы формируете $map — сначала там берутся буквы с умляутами во внутреннем представлении, затем берётся их нижний регистр, а затем каждый из них преобразуется в байтовую строку в кодировке UTF-8, и всё это склеивается обратно в список. Именно поэтому эта функция работает на первых двух строках одинаково — в первой группе utfAsBin она находит в одном случае символ в UTF-8, а в другом — во внутреннем представлении. Убираем из кода строчку
            set map [concat $map [encoding convertto utf-8 $map]]
            

            и закономерно получаем:
            
            Бинарный массив содержащий utf-8 'SchrA¶der' (как-бы двойная кодировка) ...
            Ищем A O U a o u в SchrA¶der (53636872c3b6646572) ...
            No! probably not a german text, does not contains german umlauts, found char 'A¶' (c3b6)
            
            Или это правда символы cp1252 'Schroder' но напрямую utf-8 ...
            Ищем A O U a o u в Schroder (53636872f6646572) ...
            Yes! may be german text, contains german umlaut 'o' (f6)
            
            Бинарный массив содержащий utf-8 'SchrN?der' (как-бы двойная кодировка, тот же символ, но из cp1251, не должен найти) ...
            Ищем A O U a o u в SchrN?der (53636872d186646572) ...
            No! probably not a german text, does not contains german umlauts, found char 'N?' (d186)
            
            Utf-8 (тот же символ, но из cp1251) 'Schrцder' напрямую utf-8 ... (не должен найти) ...
            Ищем A O U a o u в Schrцder (5363687246646572) ...
            No! probably not a german text, does not contains german umlauts, found char 'ц' (46)
            


            Ваш код сломается на китайских символах с вероятностью, близкой к 100%. Про какие-нибудь эмодзи я уже и не говорю.

            Ваш второй пример всего лишь демонстрирует преобразование массива байт в кодировках cp1251/cp1252 во внутреннее представление (UCS-2-совместимое) и затем обратно в массив байт в кодировке UTF-8 или UCS-2. Не очень понятно, что здесь имелось в виду.


            1. sebres
              20.05.2015 21:36

              Вы прикидваетесь? Вы абсолютно не поняли мой пример, кроме этого (чтобы утверждать, что оно не работает например на китайском) абсолютно не представляете как оно будет посмотри вы на объект, например в дебагере. Хотя возможно я просто плохо объяснил — попробую еще раз…
              1) Это не читерство — это пример. Т.е. пример мог бы быть только для чистого utf-8 или только для чистого bytearray. Я вам просто в один пример, для наглядности сложил оба варианта. Ну и искал соответсвенно по «map» доя обоих (чтобы показать где же бинарное представление). Цель — не найти умляут и так и так. А тупо не конвертировать «bin» (bytearray ли, строку ли) для поиска оного в utf-8.
              Просто в тикле нет (или не обязательно) строгое типизирование, поэтому часто одна и та-же процедура работает как для bytearray (создан encoding convertto) так и для string (создан encoding convertfrom). Совершенно разные внутренние представления объектов, если бы вы на это в дебагере посмотрели. В одном случае — это чистый utf-8 (utf-8 string representation), в другом — чистый массив байт плюс utf-8 string representation, которая либо есть сразу при создании массива, либо будет создана динамически при первом доступе к ней.
              Теперь снова про пример. Еще раз «encoding convertto» и «encoding convertfrom» — только для наглядности, чтобы вы поняли что находится в бинарном массиве (то что выводит bin2hex). Ну и если хотите, что бы пример работал независимо от системной кодировки.
              Два же варианта — binary и utf-8, что с немецкой, что с русской буквой — то-же чисто для наглядности. Если вас смущает utf-8 уберите и оставьте в примере только binary (1-й и 3-й вызов)…
              2) На основании неверно понятого (вернее неверно интерпретированного примера), вы сделали неверные (но далекоидущие) выводы.
              3) Китайский ли, немецкий ли или русский — в примере это не важно. Хотя для китайского оно работает точно также. Это если правильно понять, что я хотел сказать примером. Вы же спрашивали про «обработку binary в UTF-8-представлении».

              Из этого следует, что внутреннее представление строк в тикле ущербно
              Здесь вы тоже заблуждаетесь.
              Я уже сказал — тикль умеет несколько внутренних представлений.
              Если вы хотели сказать «внутреннее представление строк» 16-bit “Unicode” (или если хотите в UCS-2) ущербно, то, вероятно, да.
              Внутреннее же представление в utf-8 (которое есть default) никоим образом не ущербно. Обыкновенный utf-8 как оно есть везде.

              binary, которая, похоже, ASCII
              Сравнивать bytearray с ascii — это вообще улыбнуло, тем более после ваших нравоучений «программисты путают юникод и его кодировки» (вашими же словами — «путь к неправильному пониманию»). Ascii — насколько я помню 7-ми битная таблица или если хотите кодировка :)
              В тикле — binary т.е. bytearray есть массив unsigned char определенной длинны. Тупо массив байт.
              Но как и любой другой объект в тикле, он может иметь например и внутреннее представление в utf-8 (а через utf-8, может и в юникоде), или быть вообще числом неопределенной размерности, датой или float и т.д. и т.п.


        1. sebres
          20.05.2015 18:58

          Когда у программы есть ввод данных снаружи, на этом вводе всегда должна быть определена кодировка.

          Это в идеале, и не всегда возможно (или нужно), например в приведенном мною BF — фильтр fail2ban-а, на третьем питоне, проваливал найденный бан с исключением, совсем не забанив IP. Просто потому, что в логе (который utf-8) были символы в «чужой» кодировке (например сингл-байт).

          Вы же не скажете «хакеру» — вводи, например, весь урл, имя пользователя (или чего-там еще в лог пишется) только в легальном utf-8… В результате — имеем потенциальную дыру, где используя один не легитимный байт, злоумышленник может осуществлять брут, пока не подберет или админ в логи не заглянет.

          Только тссс… а то возможно не все еще обновились.

          Насчёт нужности автоматического преобразования между внутренними представлениями строки не уверен

          Это как минимум быстрее и не так вымывает кеш и т.д. Особенно для скриптовых, различающих literal и не literal объекты, при исполняемые в jit.


          1. Googolplex
            20.05.2015 19:36
            -1

            и не всегда возможно (или нужно)

            Хоть это не всегда возможно, это нужно безусловно. Если вы не знаете кодировку входных данных, вы должны либо работать с ней как с массивом байт, либо, если по каким-то причинам вы не можете зафиксировать кодировку в API, но при этом должны обрабатывать входные данные как текст (например, искать в тексте какие-то слова), то нужно пытаться интерпретировать входную последовательность байт в разных кодировках. Например, если вам нужно найти какое-то слово в тексте, а его кодировка совершенно неизвестна, то вам нужно преобразовать своё слово в десяток наиболее подходящих кодировок и искать его байтовое представление в байтах, составляющих входной текст.


            1. sebres
              20.05.2015 22:06

              Вы снова не поняли: в том же приведенном примере из fail2ban «бага» я вам объяснял, что кодировка 1) конкретно здесь абсолютно неинтересна, 2) неизвестна по определению (ясно только, что не верна или не ожидаема). Т.е. я вам про бананы (некоторая обратная несовместимость питона в обработке строк на символах в неверной кодировке привела к серьезной уязвимости пусть и из-за плохого покрытия кода). Вы мне про огурцы — кодировка прежде всего (когда оно здесь абсолютно не интересно).

              Ну вот неважно мне в этом конкретном случае, что будет стоять в DB или майле для забаненного IP:

              'nginx-http-auth', 'request: "GET /login.htm?block_f2b=\xe8\xeb\xfc\xe4\xe0\xf0 ..."', ...
              
              или:
              'nginx-http-auth', 'request: "GET /login.htm?block_f2b=?????? ..."', ...
              

              Главное — он в бане (а не перебирает дальше пароли, только потому что Гвидо с компанией решили забить здесь на обратную совместимость). А если оно все же надо будет (хотя я и в логи nginx глянуть могу), после предоставления лучшей (опциональной) возможности в 3-м питоне, поправлю код так, чтобы оно бинарно писалось или еще что. Но повторяю — опционально.

              Имхо, для скриптового языка, по умолчанию, было бы куда «надежнее» сделать здесь обратную совместимость (что игнор, что replace на "?" и т.д.). Не опционально, как сейчас, а наоборот. Хотя «о вкусах» не спорят.


  1. vsb
    20.05.2015 13:45
    +3

    Не нужно заменять символы. Заменяйте строки. Не 'W' на 'Я', а «W» на «Я». Потому что есть множество строк, которые выглядят как один символ для пользователя, но их представление хоть в 4-байтовой кодировке будет состоять из нескольких символов. Да, надо будет перераспределять строку в общем случае. Это необходимое зло. Иначе у вас будет некорректно работающий код, который рано или поздно сломается.

    Это всё подводит к тому, что UTF-8 это лучший формат для внутреннего хранения строк. Нет большой необходимости в быстрой индексации i-го символа. Очень мало алгоритмов это требуют. Для большинства случаев достаточно итерации по символам. А компактность хранения означает в том числе лучшую утилизацию кеша процессора.

    Rust, Swift выбрали именно такой способ внутреннего кодирования строк. Нет причин считать, что они ошибаются.

    А UTF-16 это худшее из обоих миров. Потому что с одной стороны не очень разбирающиеся в предмете разработчики легко напишут код, который будет корректно работать с русским языком, но сломается на китайском (не напоминает ситуацию с американскими разработчиками 20 лет назад, чей софт прекрасно работал с английским языком, а на остальных им было тогда плевать?). С другой стороны корректный код получается ровно таким же, как и в случае с UTF-8. С третьей стороны он не компактен в случае ASCII-символов (не настолько не компактен, как UTF-32, но всё равно далёк от идеала).


    1. Googolplex
      20.05.2015 13:51

      Подписываюсь под каждым словом!


  1. zenden2k
    20.05.2015 15:20
    +3

    Везде храню utf-8 строки в std::string, а std::wstring практически не использую. Доступ к отдельным символам строки мне практически нигде не нужен.


  1. SerJook
    20.05.2015 15:29
    +3

    Мне нравится подход с utf-8 строками в std::string.
    Хотя это требует много костылей, например, потребовалось реализовать fopen_utf8 для винды, также использовать преобразование в UTF-16 на винде. Также, стоит заметить, что несмотря на то, что в большинстве линуксов стандартная локаль — utf-8, это вовсе не аксиома. Поэтому опять же требуется преобразование, когда требуется.
    Но в целом, такой подход себя полностью оправдывает.

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


  1. SBKarr
    20.05.2015 15:58

    А мне понравился подход в cocos2d-x, там std::string по умолчанию считается UTF8, а для отображения на экране оно преобразуется в подобие UTF16. Подобие, поскольку это UTF16 без суррогатных пар. То есть голые 0x0000-0xFFFF (он же Plane 0, он же Basic Multilingual Plane) из юникода. Это значит, что все популярные печатные символы доступны. Если заглянуть в таблицу, Plane 0 покрывает все используемые на данный момент языки. Облом наступает с древними иероглифами и идеографикой, но, как по мне, это удел специализированных приложений.

    Идея использовать такую странную кодировку возникла не на ровном месте. Во-первых, у cocos2d-x китайские корни. Во вторых, при ручном (через Freetype) рисовании текста и работе с ним важно соответствие: символ на экране это символ в строке. Иначе алгоритм визуального удаления одной буквы становится довольно нетривиальным. С другой стороны, cocos2d-x работает на мобильных устройствах, а значит, ресурсов мало. Был найден очень разумный компромисс, как впихнуть в строки побольше читаемых символов, и занять поменьше места.


    1. Yuuri
      20.05.2015 23:49

      Такое подобие, кстати, зовётся UCS-2.


  1. Qualab
    20.05.2015 17:20

    Рад что статья вам понравилась. Действительно получилось немного похоже на QString, но хорошие решения всегда похожи. Есть ряд различий, я предлагаю внутреннее представление всё-таки на основе UTF-32 и позднюю переконвертацию (по необходимости) во внутреннее представление. Опять же подход из первой статьи позволит хранить в объекте базового класса любого его наследника, и таким образом выделить дерево текстовых типов, например для выделения текста лимитированной длины при работе с БД.


  1. lockywolf
    20.05.2015 18:16
    +1

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

    А совет автора использовать BOM с UTF-8 вызывает у меня сомнения, потому что это много какую совместимость ломает.

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

    o??? ?????????i?????n???????????v???????????o??????k?????????????e???????? ?????????t???????????h???????e??????? ?h???????????i??v???e??????????-??????m??????????i????????n?????d?????????? ???????????r??????????e????????????p?????r??????e???s???????????e????????nt???????i??????????????n??g????????? ???????c??h????????a???????o?????????s??????.????????
    ?????I??????n???????v????????o??k???????i????????????n????????????g? ????????????t??????????h?????????e?????? ?f??????e???????e?l?????????i????????n????????g??????????? ?????????o???????????f????????? ??????????????c???????????h???????a??????????????o?????????s????????.????????????
    ????W??i?????????????t??????h?????? ????????o????????u??????????t?????? ???????????o????????r??d???????er?????.????
    ?????T???????h???e????????? ????????N???e??????z????p????????e??????????????r???????d???????????i??????????a????????n????? ?????????h????????i???????????v????????e???-??????m?????????i?????????n???????d???????????? ???????o???f?????????????? ????????c???????h?????a????????o????s????????.? ????Z????????a?????????l?????????????g??????o??????????.?????
    ???H????????e???????????? ????w?????????????h?o??? ???????????W??a??????i????????t???????????s??????? ????B???????e????h????i???????n??????????????d?????? ????????T????????h??????e??????? ???????????W?????????a???l???????????l???????.????????????
    ????????Z??????A?????????L?????????????G???????O??!??


    1. Googolplex
      20.05.2015 18:30

      но важно, чтобы она предполагала фиксированное количество байт на любой символ

      Это невозможно, если под символом понимать графемный кластер. Графемный кластер может состоять из произвольного количества code point'ов. Понимать же под символом code point — очень опасное дело, особенно если вам нужно переставлять «символы».


      1. lockywolf
        20.05.2015 19:14

        Ну почему же невозможно?

        Во-первых, можно иметь априорное знание о графемных кластерах. Скажем, просто считать все слишком длинные невалидными.

        Во-вторых, можно хранить указатели или индексы.

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

        Хотя все эти решения далеки от идеала, конечно. Но в случае более-менее «приличного» текста они будут почти такие же, как интуитивно ожидаешь.


        1. Googolplex
          20.05.2015 19:46
          -1

          Почти всё то, что вы описали — это не кодировка, это способ работы с символьными данными. Кодировка — это байтовое представление символьных данных, и какую-то из них всё равно придётся выбрать (либо даже несколько и преобразовывать их по каким-то критериям на лету, как это делает, по-видимому, Tcl, хотя нужность этого весьма сомнительна).

          можно иметь априорное знание о графемных кластерах

          Нельзя. Графемные кластеры могут быть произвольными. Ограничивать их длину малым количеством code point'ов нельзя — многие валидные тексты в неанглийских языках отсекутся. Большая граница сделает размер code unit'а очень большим (десятки байт на code unit?).

          можно хранить указатели или индексы

          Это не часть кодировки.

          В-третьих, можно перестраивать представление «на лету»

          Да, можно, но этим вы не добьётесь того, что любому символу (в смысле графемному кластеру) соответствует фиксированное количество байт.

          Просто всё дело в том, что в юникоде нет взаимооднозначного соответствия между абстрактными символами и code point'ами. Более того, нет даже однозначного количества code point'ов, которые требуются для представления абстрактных символов. Любые попытки работать с юникодным текстом в любой кодировке так, как будто каждый символ занимает фиксированное число байт, обречены на провал.


          1. lockywolf
            20.05.2015 19:59

            Вы мне доказать что-то пытаетесь? Я представляю, как работает юникод.

            Десятки байт на code unit в таком случае будет возникать в довольно экзотических конфигурациях. Не то, чтобы такая конфигурация невозможна. Маловероятно, как раз то, что потребуется «быстро» «менять местами» такие code unit'ы.

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

            >>многие валидные тексты в неанглийских языках отсекутся.

            Не отсекутся, если в конструкторе спрашивать «Сколько надо байтов на юнит?».

            Кстати, а в каких языках будет много кодпойнтов на юнит? Хангыль?

            >>Да, можно, но этим вы не добьётесь того, что любому символу (в смысле графемному кластеру) соответствует фиксированное количество байт.

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


            1. Googolplex
              20.05.2015 20:32

              Кстати, а в каких языках будет много кодпойнтов на юнит?

              Это некорректный вопрос, на code unit не может быть много code point'ов. Наоборот — может быть. Я больше имел в виду тексты с диакритикой.

              Да, для подавляющего большинства операций хватит UTF-8 строк. А всякие адаптивные вещи — это настолько экзотика, что я даже не знаю, где это может пригодиться.

              Можно, вероятно, будет придумать такую строку, которая будет всё портить.

              Да, примерно это я и имел в виду. Любую строку можно разбить на графемные кластеры, взять длину максимального из них за длину code unit'а, и перекодировать строку так, чтобы каждый кластер занимал фиксированное (не больше максимального) число байт. Но, как мне кажется, это настолько специфический случай, что я даже не могу представить, где это может понадобиться.

              В целом же у нас с вами, как я вижу, консенсус)


  1. andreishe
    20.05.2015 20:23
    +2

    У вас шестеренка на рисунке какая-то неправильная. Стороны зубцов не должны быть прямыми, и дырка в середине, кажется, крупновата.