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

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 возникнут проблемы, о которых говорилось выше: многобайтные символы будут преобразовываться некорректно, и код будет ломаться в случае преобразования регистров, меняющего длины строк.

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

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

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


  1. Tantrido
    15.10.2024 07:46

    Это не C++, а Win32 API. А что ты в Линуксе будешь делать?!


    1. findoff
      15.10.2024 07:46

      ICU есть под Linux. Я именно под ним первый раз и встретился с этой библиотекой. Раньше при попытке собрать что-то старое с ней часто были проблемы, во всяком случае у меня.
      https://github.com/unicode-org/icu/releases/tag/release-75-1


      1. Tantrido
        15.10.2024 07:46

        Вторую часть абзаца я пропустил. Но тогда всю статью можно было ограничить только этой фразой:

        Если вы работаете с библиотекой International Components for Unicode (ICU), то можете использовать u_strToUpper и u_strToLower.


        1. johnfound
          15.10.2024 07:46

          И даже так: «Обязательно используйте u_strToUpper и u_strToLower из библиотеки ICU!»


  1. hippowdon
    15.10.2024 07:46

    Так Реймонд и дает рекомендацию: используйте ICU.


  1. ReadOnlySadUser
    15.10.2024 07:46

    Я бы дал более широкую рекомендацию: не используйте строки (и функции над ними) из стандартной библиотеки C++. Они просто плохи во всех отношениях.

    Максимум, на что они сгодятся - это отладочное логирование и использованием только ASCII символов. Во всех остальных случаях лучше искать библиотечные вариант (e.g. ICU/Qt)


    1. Tantrido
      15.10.2024 07:46

      Да, в Qt всё много проще!


  1. wataru
    15.10.2024 07:46

    Во-первых, std::tolower — это неадресуемая функция.

    Что-то странное. На cppreference ни слова про это нет. Там сказано, что нельзя ее исползовать напрямую, потому что ей надо, чтобы параметр был unsigned char, а в контейнерах или int или char.


    1. 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&)