четверг, 5 сентября 2019 г.

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

Это перевод The sad history of Unicode printf-style format specifiers in Visual C++. Автор: Реймонд Чен.

Windows реализовала Unicode раньше, чем большинство других операционных систем. В результате решения Windows для многих проблем отличаются от решений, принятых теми, кто подождал, когда пыль осядет¹. Самым ярким примером этого является использование Windows UCS-2 в качестве кодировки Unicode. Тогда это была кодировка, рекомендованная консорциумом Unicode, потому что Unicode 1.0 поддерживал только 65'536 символов². Консорциум Unicode передумал пять лет спустя, но к тому времени было уже слишком поздно для Windows, которая уже выпустила Win32s, Windows NT 3.1, Windows NT 3.5, Windows NT 3.51 и Windows 95 - все из которых использовали UCS-2³.

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

Windows реализовала Unicode раньше, чем сам язык C. Это означало, что Windows должна была изобрести поддержку Unicode в языке C. Результатом стали такие функции, как wcscmp, wcschr и wprintf. Что касается строк формата printf, вот что мы получили в итоге:
  • Спецификатор %s представляет строку в той же ширине (ANSI/Unicode), что и строка формата.
  • Спецификатор %S представляет строку шириной, противоположной ширине строки формата.
  • Спецификатор %hs представляет узкую (ANSI) строку независимо от ширины строки формата.
  • Спецификаторы %ws и %ls представляют широкую строку (Unicode) независимо от ширины строки формата.
Идея этого шаблона заключалась в том, чтобы вы могли написать код, подобный следующему:
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);
А если код компилировался как Unicode, результат был⁴:
wchar_t buffer[256];
GetSomeStringW(buffer, 256);
wprintf(L"The string is %s.\n", buffer);
Следуя соглашению о том, что %s принимает строку с той же шириной, что и сама строка формата, этот код выполняется правильно при компиляции и в формате ANSI и в Unicode. Это также значительно упрощает преобразование существующего кода ANSI в Unicode, так как вы можете продолжать использовать %s, и он сам преобразуется в то, что нужно.

Когда поддержка Unicode официально появилась в C99, комитет по стандартизации C выбрал другую модель для строк формата printf:
  • Спецификаторы %s и %hs представляют узкую строку.
  • Спецификатор %ls представляет широкую строку.
Это создало проблему. В экосистеме Windows прошло шесть лет, и были написаны неисчислимые миллиарды строк кода, которые использовали старую модель. Как в такой ситуации должен поступить компилятор Visual C и C++?

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

Если вы хотите, чтобы ваш код работал как в средах выполнения, использующих классические правила printf 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);
В ANSI:
char buffer[256];
GetSomeStringA(buffer, 256);
printf("The string is %hs\n", buffer);
В Unicode:
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);
Поскольку людям нравятся таблицы, то вот вам таблица:

СпецификаторКлассика WindowsСтандарт C 
%sprintfchar*char*
%swprintfwchar_t*char*
%Sprintfwchar_t*N/A
%Swprintfchar*N/A
%hsprintfchar*char*
%hswprintfchar*char*
%lsprintfwchar_t*wchar_t*
%lswprintfwchar_t*wchar_t*
%wsprintfwchar_t*N/A
%wswprintfwchar_t*N/A

Я выделил строки, в которых стандарт C соответствует классическому формату Windows⁵. Если вы хотите, чтобы ваш код работал одинаково при любом соглашении о формате, вам следует придерживаться этих строк.

Примечания:
¹ Можно подумать, что раннее внедрение Unicode даст Windows преимущество первопроходца, но, по крайней мере, в отношении Unicode, это оказалось недостатком первопроходца, потому что все остальные могут сидеть сложа руки и ждать появления более эффективных решений (таких как UTF-8) до начала их усилий по реализации поддержки Unicode.

² Я думаю, они думали, что 65'536 символов должно хватить на всех.

³ Это было позже модернизировано до UTF-16. К счастью, UTF-16 обратно совместим с UCS-2 для кодовых точек, которые представимы в обоих.

⁴ Технически, вариант с Unicode был таким:
unsigned short buffer[256];
GetSomeStringW(buffer, 256);
wprintf(L"The string is %s.\n", buffer);
потому что тогда ещё не было wchar_t как независимого типа. До введения wchar_t в стандарт C тип wchar_t был просто синонимом unsigned short. Изменяющаяся судьба типа wchar_t имеет свою собственную историю.

⁵ Классический формат Windows появился первым, именно поэтому вопрос состоит в том, выбрал ли стандарт C соответствие классическому формату Windows, а не наоборот.

Комментариев нет:

Отправить комментарий

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.