Похоже, популярный способ преобразования строки в верхний или нижний регистр заключается в побуквенном изменении.
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), поэтому она должна была вернуть DCC0. Разумеется, 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
.
Комментарии (9)
ReadOnlySadUser
15.10.2024 07:46Я бы дал более широкую рекомендацию: не используйте строки (и функции над ними) из стандартной библиотеки C++. Они просто плохи во всех отношениях.
Максимум, на что они сгодятся - это отладочное логирование и использованием только ASCII символов. Во всех остальных случаях лучше искать библиотечные вариант (e.g. ICU/Qt)
wataru
15.10.2024 07:46Во-первых,
std::tolower
— это неадресуемая функция.Что-то странное. На cppreference ни слова про это нет. Там сказано, что нельзя ее исползовать напрямую, потому что ей надо, чтобы параметр был unsigned char, а в контейнерах или int или char.
comargo
15.10.2024 07:46Наверное, потому что взять адрес от нее не тривиальная задача
https://coliru.stacked-crooked.com/view?id=300e31f9697b1fbc#include <cctype> #include <clocale> #include <iostream> #include <cxxabi.h> int main() { auto names = { typeid((int (*)(int))&std::tolower).name(), typeid(&std::tolower<char>).name(), typeid(&std::tolower<int>).name(), typeid(&std::tolower<wchar_t>).name(), typeid(&std::tolower<char32_t>).name(), typeid(&std::tolower<char8_t >).name(), typeid(&std::tolower<double>).name() }; for(const auto&name:names) { int status; std::cout << name << " = " << abi::__cxa_demangle(name, NULL, NULL, &status) << '\n'; } }
PFiiE = int (*)(int) PFccRKSt6localeE = char (*)(char, std::locale const&) PFiiRKSt6localeE = int (*)(int, std::locale const&) PFwwRKSt6localeE = wchar_t (*)(wchar_t, std::locale const&) PFDiDiRKSt6localeE = char32_t (*)(char32_t, std::locale const&) PFDuDuRKSt6localeE = char8_t (*)(char8_t, std::locale const&) PFddRKSt6localeE = double (*)(double, std::locale const&)
Tantrido
Это не C++, а Win32 API. А что ты в Линуксе будешь делать?!
findoff
ICU есть под Linux. Я именно под ним первый раз и встретился с этой библиотекой. Раньше при попытке собрать что-то старое с ней часто были проблемы, во всяком случае у меня.
https://github.com/unicode-org/icu/releases/tag/release-75-1
Tantrido
Вторую часть абзаца я пропустил. Но тогда всю статью можно было ограничить только этой фразой:
johnfound
И даже так: «Обязательно используйте u_strToUpper и u_strToLower из библиотеки ICU!»