В очередной раз убедился, что программисты пишут программы совершенно безалаберно. И работают они не благодаря их заслугам, а благодаря удачному стечению обстоятельств и заботе разработчиков компиляторов в Microsoft или Intel. Да, да, именно они заботятся и в нужный момент подставляют костылики нашим кривобоким программкам.
1. Большой брат помогает тебе
Автор: Андрей Карпов
Дата: 13.08.2010
В очередной раз убедился, что программисты пишут программы совершенно безалаберно. И
работают они не благодаря их заслугам, а благодаря удачному стечению обстоятельств и заботе
разработчиков компиляторов в Microsoft или Intel. Да, да, именно они заботятся и в нужный
момент подставляют костылики нашим кривобоким программкам.
Читайте далее байтораздирающую историю про класс CString и дочь его, функцию Format.
Молитесь, молитесь на компиляторы и их разработчиков. Они столько сил прилагают, чтобы наши
программы работали, несмотря на многие недостатки и даже ошибки. Причем эта их работа
трудна и не видна. Они - благородные рыцари кодирования и ангелы-покровители для всех нас.
Я знал, что в Microsoft существует отдел, который занимается вопросами обеспечения
максимальной совместимости новых версий операционных систем со старыми приложениями. В
их базе более 10000 наиболее известных старых программ, которые должны обязательно
работать в новых версиях Windows. Именно благодаря таким усилиям я недавно смог без
проблем поиграть в Heroes of Might and Magic II (игра 1996 года) под управлением 64-битной
Windows Vista. Думаю, игра успешно запустится и в Windows 7. Вот интересные заметки Алексея
Пахунова на тему совместимости [1, 2, 3], очень рекомендую почитать.
Но видимо существуют еще и отделы, которые занимаются тем, чтобы помочь нашему ужасному
коду на Си/Си++ работать, работать и работать. Начну эту историю с самого начала.
Я участвую в разработке инструмента PVS-Studio для анализа исходного кода приложений. Тихо,
товарищи, тихо - это не реклама. В этот раз это точно богоугодное дело, ибо мы начали создавать
бесплатный статический анализатор общего назначения. Пока даже до альфа-версии далеко, но
работы потихоньку идут и когда-нибудь я сделаю про этот анализатор пост. Заговорил я об этом
потому, что мы начали собирать наиболее интересные типовые ошибки и учиться их
диагностировать.
Множество ошибок связано с использованием в программах эллипсисов. Теоретическая справка:
Существуют функции, в описании которых невозможно указать число и типы всех
допустимых параметров. Тогда список формальных параметров завершается эллипсисом (...),
что означает: "и, возможно, еще несколько аргументов". Например:
int printf(const char* ...);
Одной такой неприятной, но легко диагностируемой ошибкой является передача в функцию с
переменным количеством аргументов объекта типа класс, вместо указателя на строку. Вот как
выглядит пример этой ошибки:
wchar_t buf[100];
std::wstring ws(L"12345");
2. swprintf(buf, L"%s", ws);
Такой код приведет к формированию в буфере белиберды или к аварийному завершению
программы. В реальной программе конечно код будет более запутанный, поэтому просьба - не
надо писать комментарии о том, что в отличие от Visual C++, компилятор GCC проверит аргументы
и предупредит. Строки могут поступать из ресурсов или других функций и проверить ничего не
удастся. Здесь же диагностика проста - в функцию формирования строки передается объект
класса, что и приводит к ошибке.
Корректный вариант кода должен выглядеть так:
wchar_t buf[100];
std::wstring ws(L"12345");
swprintf(buf, L"%s", ws.c_str());
Именно из-за того, что в функции с переменным количеством аргументов можно передать все что
угодно их и не рекомендуют использовать практически во всех книгах по программированию на
языке Си++. Вместо этого предлагается использовать безопасные механизмы, например,
boost::format. Однако рекомендации рекомендациями, а кода с разными printf, sprintf,
CString::Format огромное количество и мы с ним будем жить еще очень долго. Именно поэтому
мы и реализовали диагностическое правило, выявляющее подобные опасные конструкции.
Давайте разберемся теоретически, в чем неверен приведенный выше код. Оказывается он
некорректен дважды.
1. Несоответствие аргумента заданному формату. Раз мы указываем "%s", то и передать
должны указатель на строку. Однако теоретически мы можем написать свою функцию
sprintf, которая будет знать, что ей передан объект класса std::wstring и корректно
распечатает его. Однако и это невозможно в силу причины номер 2.
2. Аргументом для эллипсиса "..." может быть только POD-тип. А std::string POD типом не
является.
Теоретическая справка про POD типы:
POD это аббревиатура от "Plain Old Data", что можно перевести как "Простые данные в стиле
Си". К POD-типам относятся:
1. все встроенные арифметические типы (включая wchar_t и bool);
2. типы, объявленные с помощью ключевого слова enum;
3. указатели;
4. POD-структуры (struct или class) и POD-объединения (union), которые удовлетворяют
нижеприведенным требованиям:
a. не содержат пользовательских конструкторов, деструктора или копирующего
оператора присваивания;
b. не имеют базовых классов;
c. не содержат виртуальных функций;
d. не содержат защищенных (protected) или закрытых (private) нестатических
членов данных;
3. e. не содержат нестатических членов данных не-POD-типов (или массивов из
таких типов), а также ссылок.
Соответственно, класс std::wstring к POD-типам не относится, так как у него есть конструкторы,
базовый класс и так далее.
При этом если вы передаете в эллипсис объект, не являющимся POD типом, то это приводит к
неопределенному поведению. Таким образом, по крайней мере, теоретически, мы никак не
можем корректно передать объект типа std::wstring в качестве эллипсис аргумента.
Та же самая картина у нас должна наблюдаться и с функций Format из класса CString.
Некорректный вариант код:
CString s;
CString arg(L"OK");
s.Format(L"Test CString: %sn", arg);
Корректный вариант кода:
s.Format(L"Test CString: %sn", arg.GetString());
Или как предлагается в MSDN [4] для получения указателя на строку можно использовать явный
оператор приведения LPCTSTR, реализованный в классе CString. Пример корректного кода из
MSDN:
CString kindOfFruit = "bananas";
int howmany = 25;
printf("You have %d %sn", howmany, (LPCTSTR)kindOfFruit);
Итак, вроде бы все прозрачно и понятно. Как сделать правило тоже ясно. Будем обнаруживать
опечатки при использовании функций с переменным количеством аргументов.
Это и было и сделано. И вот здесь я был шокирован результатом. Оказывается большинство
разработчиков вообще никогда не задумываются над этими проблемами и спокойно пишут код
вида:
class CRuleDesc
{
CString GetProtocol();
CString GetSrcIp();
CString GetDestIp();
CString GetSrcPort();
CString GetIpDesc(CString strIp);
...
5. _stprintf(acBuf, _T("%s"),
GetResString(IDS_SV_SERVERINFO));
//---------------
// Думаю понятно,
// что примеры можно приводить и приводить.
А некоторые и задумываются, но забываются. И поэтому так трогательно смотрится код
следующего вида:
CString sAddr;
CString m_sName;
CString sTo = GetNick( hContact );
sAddr.Format(_T("%smailslot%s"),
sTo, (LPCTSTR)m_sName);
И таких примеров в проектах, на которых мы тестируем PVS-Studio, оказалась столько, что стало не
понятно, как это вообще может быть. А, тем не менее, это все замечательно работает, в чем я смог
убедиться, написав тестовую программу и попробовав различные варианты использования
CString.
В чем же дело? Видимо разработчики компиляторов не выдержали бесконечных вопросов
почему программы индусов, использующие CString не работают и обвинений в "глючности
компилятора, который неверно работает со строками". И они тихо совершили священный ритуал
экзорцизма, изгнав зло из CString. Они сделали невозможное возможным. А именно класс CString
реализован специальным хитрым образом, так, чтобы его можно было передавать в функции
вида printf, Format.
Сделано это достаточно хитро и кто интересуется, то может почитать исходный код класса
CStringT, а также познакомиться с вот эти развернутым обсуждением "Pass CString to printf?" [5].
Я вдаваться в подробности не буду. Отмечу только важный момент. Специальная реализация
CString не достаточна, теоретически передача не POD-типа приводит к непредсказуемому
поведению. Так вот разработчики Visual C++, а вместе с ними и Intel C++ сделали так, что
непредсказуемое поведение представляет из себя всегда корректный результат. :) Ведь
правильная работа программы вполне себе подмножество непредсказуемого поведения. :)
А еще я теперь начинаю задумываться над некоторыми странными особенностями поведения
компилятора при построении 64-битных программ. Есть подозрение, что разработчики
компилятора сознательно делают поведение программы не теоретическим, а практическим
6. (работоспособным), в тех простых случаях, когда они распознают некоторый паттерн. Наиболее
понятным примером может быть паттерн цикла. Пример некорректного кода:
size_t n = BigValue;
for (unsigned i = 0; i < n; i++) { ... }
Теоретически, если значение n > UINT_MAX больше, то должен возникнуть бесконечный цикл.
Однако в Release версии он не возникает, так как для переменной "i" используется 64-битный
регистр. Конечно, если код будет посложнее, то бесконечный цикл возникнет, но хотя бы в ряде
случаев программе повезет. Подробнее я писал про это в статье "64-битный конь, который
умеет считать" [6].
Раньше я думал, что такое неожиданно удачное поведение программы связано исключительно с
особенностями оптимизации Release версий. Однако теперь я в этом не уверен. Возможно, это
сознательная попытка хотя бы иногда сделать неработоспособную программу работоспособной.
Конечно, я не знаю, причина в оптимизации или в заботе большого брата, но это волне повод
пофилософствовать. :) Ну а кто знает, тот вряд ли скажет. :)
Уверен, что есть и другие моменты, когда компилятор подставляет руку программам калекам.
Если попадется что-то еще интересно, обязательно расскажу.
Желаю вам безглючного кода!
Библиографический список
1. Блог Алексея Пахунова. Обратная совместимость это серьезно.
http://www.viva64.com/go.php?url=390
2. Блог Алексея Пахунова. AppCompat. http://www.viva64.com/go.php?url=391
3. Блог Алексея Пахунова. Windows 3.x жив? http://www.viva64.com/go.php?url=392
4. MSDN. CString Operations Relating to C-Style Strings. Topic: Using CString Objects with Variable
Argument Functions . http://www.viva64.com/go.php?url=393
5. Обсуждение на сайте eggheadcafe.com. Pass CString to printf?
http://www.viva64.com/go.php?url=394
6. Андрей Карпов. 64-битный конь, который умеет считать. http://www.viva64.com/art-1-1-
1064884779.html