Печальная судьба спецификаторов формата функции printf для символов Юникода в Visual C++
Поддержка Юникода в Windows появилась раньше, чем в большинстве остальных операционных систем. Из-за этого многие проблемы, связанные с представлением символов, в Windows решались не так, как в других системах, разработчики которых отложили внедрение нового стандарта до лучших времён. Самый показательный пример: в Windows для представления символов Юникода используется кодировка UCS-2. Она была рекомендована Консорциумом Юникода, поскольку версия 1.0 поддерживала только 65 536 символов. Пять лет спустя Консорциум передумал, но к тому времени менять что-то в Windows было уже поздно, так как на рынок уже были выпущены системы Win32s, Windows NT 3.1, Windows NT 3.5, Windows NT 3.51 и Windows 95 — все они использовали кодировку UCS-2.
Но сегодня мы поговорим о строках форматирования функции printf.
Поскольку Юникод был принят в Windows раньше, чем в языке C, это означало, что разработчики Microsoft должны были придумать, как реализовать поддержку этого стандарта в среде выполнения C. В результате появились такие функции, как
- %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);
А при компиляции в режиме Юникод — такой:
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. Используйте эти спецификаторы, если хотите, чтобы ваш код выдавал одинаковые результаты в обоих форматах.
Примечания
Казалось бы, внедрение Юникода в Windows раньше прочих систем должно было дать Microsoft преимущество первого хода, но — по крайней мере в случае с Юникодом — оно обернулось для них «проклятием первопроходца», потому что остальные решили просто подождать до лучших времён, когда появятся более перспективные решения (такие как кодировка UTF-8), и только после этого внедрять Юникод в свои системы.
Видимо, они полагали, что 65 536 символов должно было хватить на всех.
Позже её заменили на UTF-16. К счастью, UTF-16 имеет обратную совместимость с UCS-2 для тех кодовых знаков, которые могут быть представлены в обеих кодировках.
Формально версия для Юникода должна выглядеть так:
unsigned short buffer[256]; GetSomeStringW(buffer, 256); wprintf(L"The string is %s.\n", buffer);
Дело в том, что wchar_t тогда ещё не был самостоятельным типом, и пока его не добавили в стандарт, он был всего лишь синонимом unsigned short. О перипетиях судьбы wchar_t можно почитать в отдельной статье.
Классический формат, разработанный Windows, появился первым, так что это скорее стандарту C пришлось подстраиваться под него, а не наоборот.
Информация была взята и дополнена с сайта: https://habr.com/ru/company/pvs-studio/blog/466875/