[Перевод] Популярный, но неправильный способ перевода строки в нижний регистр
Похоже, популярный способ преобразования строки в верхний или нижний регистр заключается в побуквенном изменении.
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 возникнут проблемы, о которых говорилось выше: многобайтные символы будут преобразовываться некорректно, и код будет ломаться в случае преобразования регистров, меняющего длины строк.
Что ж, таковы проблемы. А каким будет решение?
Если вам нужно выполнить преобразование регистра строки, то можно использовать LCMapStringEx
с LCMAP_LOWERCASE
или LCMAP_UPPERCASE
, возможно, с другими флагами наподобие LCMAP_LINGUISTIC_CASING
. Если вы работаете с библиотекой International Components for Unicode (ICU), то можете использовать u_strToUpper
и u_strToLower
.
Habrahabr.ru прочитано 15649 раз