Но сегодня мы поговорим о строках форматирования функции 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 специальный отдельный раздел, посвященный этой теме (см. «Широкие строки»). После этой статьи всё становится ещё более понятно и очевидно. Думаю, эту заметку стоит прочитать всем, кто разрабатывает кроссплатформенные приложения. Читайте и расскажите коллегам.
Комментарии (17)
amarao
10.09.2019 14:35+1Windows 95 — все они использовали кодировку UCS-2 — Windows 95 не использовала кодировку UCS-2, т.к. у неё не было никаких w-функций, только A функции (CreateWindowsW VS CreateWindowsA).
KanuTaH
10.09.2019 15:33Ну тащем-та это не совсем так, для 95/98/ME была такая штука под названием Microsoft Layer for Unicode (MSLU), в ней были w-версии всех этих функций.
Aloraman
10.09.2019 17:53Если совсем строго, то еще в 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 году.
Alexey_Alive
10.09.2019 16:42-1Зачем, вообще, использовать указатель на wchar? char вполне нормально работает и для UTF-8, и для UTF-32. А гарантировать, что один видимый символ = один символ юникода даже UTF-32 не может.
KanuTaH
10.09.2019 17:17Так-то оно так, но… насколько я понимаю, изначально wchar_t вводился для того, чтобы работали штуки типа str[i]. Это не юникод, и вообще не какой-то стандарт, а просто некое такое компиляторозависимое представление символов, что все символы имеют фиксированную длину в байтах.
ip1981
10.09.2019 17:53За прошедшие к тому моменту шесть лет для Windows было написано огромное множество программ объёмом в миллиарды строк.
Ого! Миллиарды :)
agmt
11.09.2019 08:39Благо, в 2019 наконец можно использовать нормальные char в Windows: docs.microsoft.com/en-us/windows/uwp/design/globalizing/use-utf8-code-page
Tantrido
red_andr
Прям уж «вызывающе неверная»! На самом деле «появилась раньше, чем в большинстве» никак не противоречит «раньше всего появилась в Линуксе». «Большинство» же не значит «все остальные». И да, когда конкретно поддержка Юникода в Линуксе появилась?
apro
Если имеется ввиду использование utf-8 в Linux то это теоретически невозможно.
utf-8 в том виде который мы ее знаем была разработана в рамках работы над ОС Plan-9,
поэтому естественно первая ОС с utf-8 это Plan-9 и в Linux utf-8 могла появиться только позже Plan-9.
Sabubu
В линуксе поддержка Юникода оставала от Windows. utf-8 они использовали, так как было огромное количество программ, рассчитанных на 8-битные кодировки.
Tantrido
В чём отставала?! В линуксе давно на UTF-8 сидел, когда в винде (параллельно стояла) ещё и в помине не было, может возможность была, но в приложениях, ни в локали, ни ФС (и сейчас кажись нету) не было.
VEG
Мир не ограничивался Windows 95/98, которые были временным решением для домашних пользователей, пока линейка NT созревала для этих целей.
Windows NT 3.1 вышла в 1993, и она уже поддерживала Unicode. NTFS появилась тогда же, и она изначально поддерживает исключительно Unicode.
Tantrido
Удивительно! :) Значит я пропустил: я сидел на пользовательских 3.1/95/98/XP… и там с кодировками была ж…
jaiprakash
2000 и XP из ветки NT, и там можно было использовать NTFS.