﻿# Большой брат помогает тебе

В очередной раз убедился, что программисты пишут программы совершенно безалаберно\. И работают они не благодаря их заслугам, а благодаря удачному стечению обстоятельств и заботе разработчиков компиляторов в Microsoft или Intel\. Да, да, именно они заботятся и в нужный момент подставляют костылики нашим кривобоким программкам\.

Читайте далее байтораздирающую историю про класс CString и дочь его, функцию Format\.

Молитесь, молитесь на компиляторы и их разработчиков\. Они столько сил прилагают, чтобы наши программы работали, несмотря на многие недостатки и даже ошибки\. Причем эта их работа трудна и не видна\. Они \- благородные рыцари кодирования и ангелы\-покровители для всех нас\.

Я знал, что в Microsoft существует отдел, который занимается вопросами обеспечения максимальной совместимости новых версий операционных систем со старыми приложениями\. В их базе более 10000 наиболее известных старых программ, которые должны обязательно работать в новых версиях Windows\. Именно благодаря таким усилиям я недавно смог без проблем поиграть в Heroes of Might and Magic II \(игра 1996 года\) под управлением 64\-битной Windows Vista\. Думаю, игра успешно запустится и в Windows 7\. Вот интересные заметки Алексея Пахунова на тему совместимости \[[1](https://blog.not-a-kernel-guy.com/2007/01/17/136/), [2](https://blog.not-a-kernel-guy.com/2009/12/08/703/), [3](https://blog.not-a-kernel-guy.com/2007/01/12/133/)\], очень рекомендую почитать\.

Но видимо существуют еще и отделы, которые занимаются тем, чтобы помочь нашему ужасному коду на Си/Си\+\+ работать, работать и работать\. Начну эту историю с самого начала\.

Я участвую в разработке инструмента [PVS\-Studio](https://pvs-studio.ru/ru/pvs-studio/) для анализа исходного кода приложений\. Тихо, товарищи, тихо \- это не реклама\. В этот раз это точно богоугодное дело, ибо мы начали создавать бесплатный статический анализатор общего назначения\. Пока даже до альфа\-версии далеко, но работы потихоньку идут и когда\-нибудь я сделаю про этот анализатор пост\. Заговорил я об этом потому, что мы начали собирать наиболее интересные типовые ошибки и учиться их диагностировать\.

Множество ошибок связано с использованием в программах эллипсисов\. Теоретическая справка:

_Существуют функции, в описании которых невозможно указать число и типы всех допустимых параметров\. Тогда список формальных параметров завершается эллипсисом \(\.\.\.\), что означает: "и, возможно, еще несколько аргументов"\. Например: int printf\(const char\* \.\.\.\);_

Одной такой неприятной, но легко диагностируемой ошибкой является передача в функцию с переменным количеством аргументов объекта типа класс, вместо указателя на строку\. Вот как выглядит пример этой ошибки:

```cpp
wchar_t buf[100];
std::wstring ws(L"12345");
swprintf(buf, L"%s", ws);
```

Такой код приведет к формированию в буфере белиберды или к аварийному завершению программы\. В реальной программе конечно код будет более запутанный, поэтому просьба \- не надо писать комментарии о том, что в отличие от Visual C\+\+, компилятор GCC проверит аргументы и предупредит\. Строки могут поступать из ресурсов или других функций и проверить ничего не удастся\. Здесь же диагностика проста \- в функцию формирования строки передается объект класса, что и приводит к ошибке\.

Корректный вариант кода должен выглядеть так:

```cpp
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\.
1. Аргументом для эллипсиса "\.\.\." может быть только POD\-тип\. А std::string POD типом не является\.

Теоретическая справка про POD типы:

_POD это аббревиатура от "Plain Old Data", что можно перевести как "Простые данные в стиле Си"\. К POD\-типам относятся:_

1. _все встроенные арифметические типы \(включая wchar\_t и bool\);_
1. _типы, объявленные с помощью ключевого слова enum;_
1. _указатели;_
1. _POD\-структуры \(struct или class\) и POD\-объединения \(union\), которые удовлетворяют нижеприведенным требованиям:_
    * _не содержат пользовательских конструкторов, деструктора или копирующего оператора присваивания;_
    * _не имеют базовых классов;_
    * _не содержат виртуальных функций;_
    * _не содержат защищенных \(protected\) или закрытых \(private\) нестатических членов данных;_
    * _не содержат нестатических членов данных не\-POD\-типов \(или массивов из таких типов\), а также ссылок\._

Соответственно, класс std::wstring к POD\-типам не относится, так как у него есть конструкторы, базовый класс и так далее\.

При этом если вы передаете в эллипсис объект, не являющимся POD типом, то это приводит к неопределенному поведению\. Таким образом, по крайней мере, теоретически, мы никак не можем корректно передать объект типа std::wstring в качестве эллипсис аргумента\.

Та же самая картина у нас должна наблюдаться и с функций Format из класса CString\. Некорректный вариант код:

```cpp
CString s;
CString arg(L"OK");
s.Format(L"Test CString: %s\n", arg);
```

Корректный вариант кода:

```cpp
s.Format(L"Test CString: %s\n", arg.GetString());
```

Или как предлагается в MSDN \[[4](https://www.microsoft.com/en-us/download/details.aspx?id=55979)\] для получения указателя на строку можно использовать явный оператор приведения LPCTSTR, реализованный в классе CString\. Пример корректного кода из MSDN:

```cpp
CString kindOfFruit = "bananas";
int howmany = 25;
printf("You have %d %s\n", howmany, (LPCTSTR)kindOfFruit);
```

Итак, вроде бы все прозрачно и понятно\. Как сделать правило тоже ясно\. Будем обнаруживать опечатки при использовании функций с переменным количеством аргументов\.

Это и было и сделано\. И вот здесь я был шокирован результатом\. Оказывается большинство разработчиков вообще никогда не задумываются над этими проблемами и спокойно пишут код вида:

```cpp
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));

//---------------

// Думаю понятно,
// что примеры можно приводить и приводить.
```



А некоторые и задумываются, но забываются\. И поэтому так трогательно смотрится код следующего вида:

```cpp
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\. Я вдаваться в подробности не буду\. Отмечу только важный момент\. Специальная реализация CString не достаточна, теоретически передача не POD\-типа приводит к непредсказуемому поведению\. Так вот разработчики Visual C\+\+, а вместе с ними и Intel C\+\+ сделали так, что непредсказуемое поведение представляет из себя всегда корректный результат\. :\) Ведь правильная работа программы вполне себе подмножество непредсказуемого поведения\. :\)

А еще я теперь начинаю задумываться над некоторыми странными особенностями поведения компилятора при построении 64\-битных программ\. Есть подозрение, что разработчики компилятора сознательно делают поведение программы не теоретическим, а практическим \(работоспособным\), в тех простых случаях, когда они распознают некоторый паттерн\. Наиболее понятным примером может быть паттерн цикла\. Пример некорректного кода:

```cpp
size_t n = BigValue;
for (unsigned i = 0; i < n; i++) { ... }
```

Теоретически, если значение n \> UINT\_MAX больше, то должен возникнуть бесконечный цикл\. Однако в Release версии он не возникает, так как для переменной "i" используется 64\-битный регистр\. Конечно, если код будет посложнее, то бесконечный цикл возникнет, но хотя бы в ряде случаев программе повезет\. Подробнее я писал про это в статье "[64\-битный конь, который умеет считать](https://pvs-studio.ru/ru/blog/posts/cpp/a0043/)" \[6\]\.

Раньше я думал, что такое неожиданно удачное поведение программы связано исключительно с особенностями оптимизации Release версий\. Однако теперь я в этом не уверен\. Возможно, это сознательная попытка хотя бы иногда сделать неработоспособную программу работоспособной\. Конечно, я не знаю, причина в оптимизации или в заботе большого брата, но это волне повод пофилософствовать\. :\) Ну а кто знает, тот вряд ли скажет\. :\)

Уверен, что есть и другие моменты, когда компилятор подставляет руку программам калекам\. Если попадется что\-то еще интересно, обязательно расскажу\.

Желаю вам безглючного кода\!

## Библиографический список

1. Блог Алексея Пахунова\. [Обратная совместимость — это серьезно](https://blog.not-a-kernel-guy.com/2007/01/17/136/)\.
1. Блог Алексея Пахунова\. [AppCompat](https://blog.not-a-kernel-guy.com/2009/12/08/703/)\.
1. Блог Алексея Пахунова\. [Windows 3\.x жив?](https://blog.not-a-kernel-guy.com/2007/01/12/133/)
1. MSDN\. CString Operations Relating to C\-Style Strings\. Topic: [Using CString Objects with Variable Argument Functions](https://www.microsoft.com/en-us/download/details.aspx?id=55979) \.
1. Андрей Карпов\. [64\-битный конь, который умеет считать](https://pvs-studio.ru/ru/blog/posts/cpp/a0043/)\.