Печальная судьба спецификаторов формата функции printf для символов Юникода в Visual C++

Автор оригинала: Raymond Chen
  • Перевод
Поддержка Юникода в Windows появилась раньше, чем в большинстве остальных операционных систем. Из-за этого многие проблемы, связанные с представлением символов, в Windows решались не так, как в других системах, разработчики которых отложили внедрение нового стандарта до лучших времён [1]. Самый показательный пример: в Windows для представления символов Юникода используется кодировка UCS-2. Она была рекомендована Консорциумом Юникода, поскольку версия 1.0 поддерживала только 65 536 символов [2]. Пять лет спустя Консорциум передумал, но к тому времени менять что-то в Windows было уже поздно, так как на рынок уже были выпущены системы Win32s, Windows NT 3.1, Windows NT 3.5, Windows NT 3.51 и Windows 95 — все они использовали кодировку UCS-2 [3].

Но сегодня мы поговорим о строках форматирования функции printf.

Поскольку Юникод был принят в Windows раньше, чем в языке C, это означало, что разработчики Microsoft должны были придумать, как реализовать поддержку этого стандарта в среде выполнения C. В результате появились такие функции, как wcscmp, wcschr и wprintf. Что же касается строк форматирования в printf, то для них ввели следующие спецификаторы:

  • %s представляет строку той же ширины, что и строка форматирования;
  • %S представляет строку с шириной, обратной ширине строки форматирования;
  • %hs представляет обычную строку независимо от ширины строки форматирования;
  • %ws и %ls представляют широкую строку независимо от ширины строки форматирования.

Идея состояла в том, чтобы можно было написать такой код:

TCHAR buffer[256];
GetSomeString(buffer, 256);
_tprintf(TEXT("The string is %s.\n"), buffer);

И при компиляции в режиме ANSI получить вот такой результат:

char buffer[256];
GetSomeStringA(buffer, 256);
printf("The string is %s.\n", buffer);

А при компиляции в режиме Юникод — такой [4]:

wchar_t buffer[256];
GetSomeStringW(buffer, 256);
wprintf(L"The string is %s.\n", buffer);

Поскольку спецификатор %s принимает строку той же ширины, что у строки форматирования, такой код будет работать корректно и в формате ANSI, и в формате Юникод. Также это решение очень упрощает преобразование уже написанного кода из формата ANSI в формат Юникод, так как на место спецификатора %s подставляется строка нужной ширины.

Когда поддержка Юникода была официально добавлена в C99, комитет по стандартизации языка C принял другую модель строк форматирования для функции printf:

  • %s и %hs представляют обычную строку;
  • %ls представляет широкую строку.

Тут-то и начались проблемы. За прошедшие к тому моменту шесть лет для Windows было написано огромное множество программ объёмом в миллиарды строк, и в них использовался старый формат. Как быть компиляторам Visual C и C++?

Было решено остаться на старой, нестандартной модели, чтобы не сломать все существующие в мире программы под Windows.

Если вы хотите, чтобы ваш код работал и в тех средах исполнения, которые придерживаются классических правил для printf, и в тех, которые следуют правилам стандарта C, вам придётся ограничиться спецификаторами %hs для обычных строк и %ls для широких. В этом случае гарантируется постоянство результатов, независимо от того, передаётся строка форматирования в функцию sprintf или wsprintf.

#ifdef UNICODE
#define TSTRINGWIDTH TEXT("l")
#else
#define TSTRINGWIDTH TEXT("h")
#endif
TCHAR buffer[256];
GetSomeString(buffer, 256);
_tprintf(TEXT("The string is %") TSTRINGWIDTH TEXT("s\n"), buffer);
char buffer[256];
GetSomeStringA(buffer, 256);
printf("The string is %hs\n", buffer);
wchar_t buffer[256];
GetSomeStringW(buffer, 256);
wprintf("The string is %ls\n", buffer);

Вынесенное отдельно определение TSTRINGWIDTH позволяет писать, например, вот такой код:

_tprintf(TEXT("The string is %10") TSTRINGWIDTH TEXT("s\n"), buffer);

Поскольку людям нравится табличное представление информации, вот вам таблица.


Я выделил строки со спецификаторами, которые в C определены так же, как и в классическом формате, принятом в Windows [5]. Используйте эти спецификаторы, если хотите, чтобы ваш код выдавал одинаковые результаты в обоих форматах.

Примечания

[1] Казалось бы, внедрение Юникода в Windows раньше прочих систем должно было дать Microsoft преимущество первого хода, но — по крайней мере в случае с Юникодом — оно обернулось для них «проклятием первопроходца», потому что остальные решили просто подождать до лучших времён, когда появятся более перспективные решения (такие как кодировка UTF-8), и только после этого внедрять Юникод в свои системы.

[2] Видимо, они полагали, что 65 536 символов должно было хватить на всех.

[3] Позже её заменили на UTF-16. К счастью, UTF-16 имеет обратную совместимость с UCS-2 для тех кодовых знаков, которые могут быть представлены в обеих кодировках.

[4] Формально версия для Юникода должна выглядеть так:

unsigned short buffer[256];
GetSomeStringW(buffer, 256);
wprintf(L"The string is %s.\n", buffer);

Дело в том, что wchar_t тогда ещё не был самостоятельным типом, и пока его не добавили в стандарт, он был всего лишь синонимом unsigned short. О перипетиях судьбы wchar_t можно почитать в отдельной статье.

[5] Классический формат, разработанный Windows, появился первым, так что это скорее стандарту C пришлось подстраиваться под него, а не наоборот.

Примечание переводчика

Я благодарен автору за эту публикацию. Теперь стало понятно, как получилась вся эта путаница с "%s". Дело в то том, что наши пользователи постоянно задавали вопрос, почему PVS-Studio по-разному реагирует на их, как им кажется, «переносимый» код, в зависимости собирают они свой проект под Linux или Windows. Понадобилось сделать в описании диагностики V576 специальный отдельный раздел, посвященный этой теме (см. «Широкие строки»). После этой статьи всё становится ещё более понятно и очевидно. Думаю, эту заметку стоит прочитать всем, кто разрабатывает кроссплатформенные приложения. Читайте и расскажите коллегам.
PVS-Studio
470,24
Static Code Analysis for C, C++, C# and Java
Поделиться публикацией

Похожие публикации

Комментарии 22

    –1
    Поддержка Юникода в Windows появилась раньше, чем в большинстве остальных операционных систем.
    Вызывающе неверная информация: раньше всего появилась в Linux-е (сразу всё в комплексе локаль, приложения, библиотеки и ФС и т.д.), а в Windows гораздо позже, неполноценно и криво (на ФС не было).
      +2
      Прям уж «вызывающе неверная»! На самом деле «появилась раньше, чем в большинстве» никак не противоречит «раньше всего появилась в Линуксе». «Большинство» же не значит «все остальные». И да, когда конкретно поддержка Юникода в Линуксе появилась?
        +3
        Вызывающе неверная информация: раньше всего появилась в Linux

        Если имеется ввиду использование utf-8 в Linux то это теоретически невозможно.
        utf-8 в том виде который мы ее знаем была разработана в рамках работы над ОС Plan-9,
        поэтому естественно первая ОС с utf-8 это Plan-9 и в Linux utf-8 могла появиться только позже Plan-9.

          +1

          В линуксе поддержка Юникода оставала от Windows. utf-8 они использовали, так как было огромное количество программ, рассчитанных на 8-битные кодировки.

            +1
            В чём отставала?! В линуксе давно на UTF-8 сидел, когда в винде (параллельно стояла) ещё и в помине не было, может возможность была, но в приложениях, ни в локали, ни ФС (и сейчас кажись нету) не было.
              +3
              Мир не ограничивался Windows 95/98, которые были временным решением для домашних пользователей, пока линейка NT созревала для этих целей.

              Windows NT 3.1 вышла в 1993, и она уже поддерживала Unicode. NTFS появилась тогда же, и она изначально поддерживает исключительно Unicode.
                –2
                Удивительно! :) Значит я пропустил: я сидел на пользовательских 3.1/95/98/XP… и там с кодировками была ж…
                  +1
                  2000 и XP из ветки NT, и там можно было использовать NTFS.
          +3

          Windows 95 — все они использовали кодировку UCS-2 — Windows 95 не использовала кодировку UCS-2, т.к. у неё не было никаких w-функций, только A функции (CreateWindowsW VS CreateWindowsA).

            0
            Ну тащем-та это не совсем так, для 95/98/ME была такая штука под названием Microsoft Layer for Unicode (MSLU), в ней были w-версии всех этих функций.
              +2
              Если совсем строго, то еще в Win 3.x вместе с родным Win16 API было еще некоторое подмножество Win32 (Win32S = Win32 Subset), в котором A-функции шли вместе с W-функциями, только вместо имплементаций у W-функций там были заглушки, возвращающие ошибки.
              95/98/ME хоть и имели формально полную Win32-подсистему, но большинство W-функций там тоже шло заглушками. Большинство, но не все. Некоторые функции (их довольно мало) для работы с ресурсами (e.g. EnumResourceNamesW), командной строкой (e.g. GetCommandLineW), диалоговыми сообщениями (e.g. MessageBoxW) имели рабочую реализацию, также были доступны вспомогательные функции для конвертации W-строк в A-строки (MultiByteToWideChar, WideCharToMultiByte) с кодированием MBCS (Multibyte Character Set). В 98 добавили еще чуток функций (lstrlenW, lstrcpyW, lstrcatW). Полноценная поддержка Win32 через MSLU (так же известная как unicows) для них появилась только в 2001 году.
              +4

              Unicode != wchar_t.

                0
                Зачем, вообще, использовать указатель на wchar? char вполне нормально работает и для UTF-8, и для UTF-32. А гарантировать, что один видимый символ = один символ юникода даже UTF-32 не может.
                  +2
                  Так-то оно так, но… насколько я понимаю, изначально wchar_t вводился для того, чтобы работали штуки типа str[i]. Это не юникод, и вообще не какой-то стандарт, а просто некое такое компиляторозависимое представление символов, что все символы имеют фиксированную длину в байтах.
                    +1
                    > char вполне нормально работает и для UTF-8, и для UTF-32

                    char, если он 8-битный, не работает для UTF-32 так, как надо: нельзя прочитать/записать один кодовый пункт целиком, нельзя проверять на NUL ограниченные им строки, нельзя простым ++ или — перейти на следующий/предыдущий пункт.
                    Для этого всего нужен char32_t вместо char.

                    > А гарантировать, что один видимый символ = один символ юникода даже UTF-32 не может.

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

                      нельзя простым ++ или -- перейти на следующий/предыдущий пункт
                      Но и не сильно это сложно:
                      if (*ptr++ & 0x80) while ((*ptr & 0xC0) == 0x80) ptr++;
                        +2
                        > NUL не может появиться внутри другого символа, это всегда отдельный байт.
                        > if (*ptr++ & 0x80) while ((*ptr & 0xC0) == 0x80) ptr++;

                        Это вы всё говорили про UTF-8, а я про UTF-32.
                    0
                    За прошедшие к тому моменту шесть лет для Windows было написано огромное множество программ объёмом в миллиарды строк.

                    Ого! Миллиарды :)

                      +1
                      Пора уже забыть про TCHAR и, соответственно, _tprintf, и, хуже того, _tmain.
                        0
                        Благо, в 2019 наконец можно использовать нормальные char в Windows: docs.microsoft.com/en-us/windows/uwp/design/globalizing/use-utf8-code-page
                          0
                          А где ссылки из статьи? А то есть [1], [2], а самого списка ссылок нету.
                            0
                            Это не ссылки, а примечания. См. ниже раздел «Примечания».

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое