
Молитесь, молитесь на компиляторы и их разработчиков. Они столько сил прилагают, чтобы наши программы работали, несмотря на многие недостатки и даже ошибки. Причем эта их работа трудна и не видна. Они — благородные рыцари кодирования и ангелы-покровители для всех нас.
Я знал, что в 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"); 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 огромное количество и мы с ним будем жить еще очень долго. Именно поэтому мы и реализовали диагностическое правило, выявляющее подобные опасные конструкции.
Давайте разберемся теоретически, в чем неверен приведенный выше код. Оказывается он некорректен дважды.
- Несоответствие аргумента заданному формату. Раз мы указываем "%s", то и передать должны указатель на строку. Однако теоретически мы можем написать свою функцию sprintf, которая будет знать, что ей передан объект класса std::wstring и корректно распечатает его. Однако и это невозможно в силу причины номер 2.
- Аргументом для эллипсиса "..." может быть только POD-тип. А std::string POD типом не является.
Теоретическая справка про POD типы:
POD это аббревиатура от «Plain Old Data», что можно перевести как «Простые данные в стиле Си». К POD-типам относятся:
- все встроенные арифметические типы (включая wchar_t и bool);
- типы, объявленные с помощью ключевого слова enum;
- указатели;
- POD-структуры (struct или class) и POD-объединения (union), которые удовлетворяют нижеприведенным требованиям:
- не содержат пользовательских конструкторов, деструктора или копирующего оператора присваивания;
- не имеют базовых классов;
- не содержат виртуальных функций;
- не содержат защищенных (protected) или закрытых (private) нестатических членов данных;
- не содержат нестатических членов данных не-POD-типов (или массивов из таких типов), а также ссылок.
Соответственно, класс std::wstring к POD-типам не относится, так как у него есть конструкторы, базовый класс и так далее.
При этом если вы передаете в эллипсис объект, не являющийся POD типом, то это приводит к неопределенному поведению. Таким образом, по крайней мере, теоретически, мы никак не можем корректно передать объект типа std::wstring в качестве эллипсис аргумента.
Та же самая картина у нас должна наблюдаться и с функций Format из класса CString. Некорректный вариант код:
CString s; CString arg(L"OK"); s.Format(L"Test CString: %s\n", arg);
Корректный вариант кода:
s.Format(L"Test CString: %s\n", arg.GetString());
Или как предлагается в MSDN [4] для получения указателя на строку можно использовать явный оператор приведения LPCTSTR, реализованный в классе CString. Пример корректного кода из MSDN:
CString kindOfFruit = "bananas"; int howmany = 25; printf("You have %d %s\n", howmany, (LPCTSTR)kindOfFruit);
Итак, вроде бы все прозрачно и понятно. Как сделать правило тоже ясно. Будем обнаруживать опечатки при использовании функций с переменным количеством аргументов.
Это и было и сделано. И вот здесь я был шокирован результатом. Оказывается большинство разработчиков вообще никогда не задумываются над этими проблемами и спокойно пишут код вида:
class CRuleDesc { CString GetProtocol(); CString GetSrcIp(); CString GetDestIp(); CString GetSrcPort(); CString GetIpDesc(CString strIp); ... CString CRuleDesc::GetRuleDesc() { CString strDesc; strDesc.Format( _T("%s all network traffic from <br>%s " "on %s<br>to %s on %s <br>for the %s"), GetAction(), GetSrcIp(), GetSrcPort(), GetDestIp(), GetDestPort(), GetProtocol()); return strDesc; } //--------------- CString strText; CString _strProcName(L""); ... strText.Format(_T("%s"), _strProcName); //--------------- CString m_strDriverDosName; CString m_strDriverName; ... m_strDriverDosName.Format( _T("\\\\.\\%s"), m_strDriverName); //--------------- CString __stdcall GetResString(UINT dwStringID); ... _stprintf(acBuf, _T("%s"), GetResString(IDS_SV_SERVERINFO)); //--------------- // Думаю понятно, // что примеры можно приводить и приводить.
А некоторые и задумываются, но забываются. И поэтому так трогательно смотрится код следующего вида:
CString sAddr; CString m_sName; CString sTo = GetNick( hContact ); sAddr.Format(_T("\\\\%s\\mailslot\\%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-битных программ. Есть подозрение, что разработчики компилятора сознательно делают поведение программы не теоретическим, а практическим (работоспособным), в тех простых случаях, когда они распознают некоторый паттерн. Наиболее понятным примером может быть паттерн цикла. Пример некорректного кода:
size_t n = BigValue; for (unsigned i = 0; i < n; i++) { ... }
Теоретически, если значение n > UINT_MAX больше, то должен возникнуть бесконечный цикл. Однако в Release версии он не возникает, так как для переменной «i» используется 64-битный регистр. Конечно, если код будет посложнее, то бесконечный цикл возникнет, но хотя бы в ряде случаев программе повезет. Подробнее я писал про это в статье "64-битный конь, который умеет считать" [6].
Раньше я думал, что такое неожиданно удачное поведение программы связано исключительно с особенностями оптимизации Release версий. Однако теперь я в этом не уверен. Возможно, это сознательная попытка хотя бы иногда сделать неработоспособную программу работоспособной. Конечно, я не знаю, причина в оптимизации или в заботе большого брата, но это волне повод пофилософствовать. :) Ну а кто знает, тот вряд ли скажет. :)
Уверен, что есть и другие моменты, когда компилятор подставляет руку программам калекам. Если попадется что-то еще интересно, обязательно расскажу.
Желаю вам безглючного кода!
Библиографический список
- Блог Алексея Пахунова. Обратная совместимость это серьезно. http://www.viva64.com/go.php?url=390
- Блог Алексея Пахунова. AppCompat. http://www.viva64.com/go.php?url=391
- Блог Алексея Пахунова. Windows 3.x жив? http://www.viva64.com/go.php?url=392
- MSDN. CString Operations Relating to C-Style Strings. Topic: Using CString Objects with Variable Argument Functions. http://www.viva64.com/go.php?url=393
- Обсуждение на сайте eggheadcafe.com. Pass CString to printf? http://www.viva64.com/go.php?url=394
- Андрей Карпов. 64-битный конь, который умеет считать. http://www.viva64.com/art-1-1-1064884779.html