[Перевод] Популярный, но неправильный способ перевода строки в нижний регистр

5075adcb818da980766b5cfc36a27c17.png

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

std::wstring name;

std::transform(name.begin(), name.end(), name.begin(),
    std::tolower);

Но он ошибочен по многим причинам.

Во-первых, std::tolower — это неадресуемая функция. Среди прочего, это значит, что мы не можем брать адрес функции¹, как мы делаем это здесь, когда передаём указатель на функцию std::transform. То есть нам нужно использовать лямбду.

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

std::wstring name;

std::transform(name.begin(), name.end(), name.begin(),
    [](auto c) { return std::tolower(c); });

Следующая ошибка — это копипастинг: код использует std::tolower для преобразования широких символов (wchar_t), хотя std::tolower работает только для узких символов (и даже ещё строже: он работает только для беззнаковых узких символов unsigned char). Ошибки на этапе компиляции не возникает, потому что std::tolower принимает int, а в большинстве систем  wchar_t неявным образом переводится в int, поэтому компилятор принимает значение без жалоб, хотя и 99% потенциальных значений находится вне нужного диапазона.

Даже если мы исправим код, добавив std::towlower:

std::wstring name;

std::transform(name.begin(), name.end(), name.begin(),
    [](auto c) { return std::towlower(c); });

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

Если для wchar_t используется кодировка UTF-16, то символы вне основной многоязыковой плоскости (basic multilingual plane, BMP) представлены парами значений wchar_t. Например, Unicode-символ OLD HUNGARIAN CAPITAL LETTER A² (U+10C80) представлен двумя кодовыми единицами UTF-16:  D803 и DC80.

² Мне кажется старомодным, что названия Unicode-символов записываются ОДНИМИ ЗАГЛАВНЫМИ БУКВАМИ, как будто на случай, если нужно будет отправить их в телеграмме, передаваемой кодом Бодо.

Если передавать эти две кодовые единицы towlower по одной за раз, то towlower не поймёт, как они связаны друг с другом. Если вызвать towlower с DC80, то она поймёт, что ей передали только половину символа, но не будет знать, какой должна быть вторая половина, поэтому просто «пожмёт плечами» и скажет: «Ну что ж, DC80?» Не повезло, потому что версия OLD HUNGARIAN CAPITAL LETTER A (U+10C80) в нижнем регистре — это OLD HUNGARIAN SMALL LETTER A (U+10CC0), поэтому она должна была вернуть. Разумеется, towlower не может читать мысли, поэтому нельзя ожидать от неё угадывания того, что DC80 — это пара для не встречавшегося ей D803.

Ещё одна проблема (которая актуальна, даже если wchar_t закодирован UTF-32) заключается в том, что версии символа в верхнем и нижнем регистре могут иметь разную длину. Например, LATIN SMALL LETTER SHARP S («ß» U+00DF) при преобразовании в верхний регистр становится двухсимвольной последовательностью «SS»: ³ Straße ⇒ STRASSE, а LATIN SMALL LIGATURE FL («fl» U+FB02) преобразуется в верхнем регистре в двухсимвольную последовательность «FL». В обоих примерах преобразование строки в верхний регистр делает строку длиннее. А в некоторых формах французского языка преобразование символа с диакритикой в заглавную букву вызывает пропадание диакритики: à Paris ⇒ A PARIS. Если символ с диакритикой à кодировался как LATIN SMALL LETTER A (U+0061), за которым следовал COMBINING GRAVE ACCENT (U+0300), то преобразование в верхний регистр приведёт к укорачиванию строки.

³ По правилам, действовавшим до 1996 года, при определённых условиях символ ß в заглавные буквы преобразовывался как «SZ»: Maßen ⇒ MASZEN. А в 2017 году Совет по орфографии немецкого языка (Rat für deutsche Rechtschreibung) разрешил использовать LATIN CAPITAL LETTER SHARP S («ẞ» U+1E9E) в качестве заглавной формы ß.

Схожие проблемы актуальны и для версии std::string:

std::string name;

std::transform(name.begin(), name.end(), name.begin(),
    [](auto c) { return std::tolower(c); });

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

Что ж, таковы проблемы. А каким будет решение?

Если вам нужно выполнить преобразование регистра строки, то можно использовать LCMap­String­Ex с LCMAP_LOWERCASE или LCMAP_UPPERCASE, возможно, с другими флагами наподобие LCMAP_LINGUISTIC_CASING. Если вы работаете с библиотекой International Components for Unicode (ICU), то можете использовать u_strToUpper и u_strToLower.

Habrahabr.ru прочитано 15649 раз