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

Что такое современный C ++?

Термин современный C ++ стал очень популярным после выпуска C ++ 11. Что это означает? Прежде всего, современный C ++ - это набор шаблонов и идиом, которые предназначены для устранения недостатков старого доброго «C с классами», к которому привыкли многие программисты на C ++, особенно если они начали программировать на C. C ++ 11 выглядит более лаконичным и понятным, что очень важно.

Что обычно думают люди, когда говорят о современном C ++? Параллелизм, вычисление во время компиляции, RAII, лямбды, диапазоны, концепции, модули и другие не менее важные компоненты стандартной библиотеки (например, API для работы с файловой системой). Все это очень крутые модернизации, и мы с нетерпением ждем их появления в следующем наборе стандартов. Однако я хотел бы обратить внимание на то, как новые стандарты позволяют писать более безопасный код. При разработке статического анализатора мы видим множество различных ошибок, и порой невольно думаем: «Но в современном C ++ этого можно было бы избежать». Поэтому предлагаю изучить несколько ошибок, которые PVS-Studio обнаружил в различных проектах с открытым исходным кодом. Также мы увидим, как их можно исправить.

Автоматический вывод типа

В C ++ были добавлены ключевые слова auto и decltype. Конечно, вы уже знаете, как они работают.

std::map<int, int> m;
auto it = m.find(42);
//C++98: std::map<int, int>::iterator it = m.find(42);

Очень удобно сокращать длинные типы без потери читабельности кода. Однако эти ключевые слова становятся довольно обширными, вместе с шаблонами: нет необходимости указывать тип возвращаемого значения с помощью auto и decltype.

Но вернемся к нашей теме. Вот пример 64-битной ошибки:

string str = .....;
unsigned n = str.find("ABC");
if (n != string::npos)

В 64-битном приложении значение string :: npos больше максимального значения UINT_MAX, которое может быть представлено переменной unsigned введите. Может показаться, что это тот случай, когда auto может спасти нас от такого рода проблем: тип переменной n для нас не важен, главное - что он может вместить все возможные значения string :: find. И действительно, если мы перепишем этот пример с помощью auto, ошибка исчезнет:

string str = .....;
auto n = str.find("ABC");
if (n != string::npos)

Но не все так просто. Использование auto не является панацеей, и есть много подводных камней, связанных с его использованием. Например, вы можете написать такой код:

auto n = 1024 * 1024 * 1024 * 5;
char* buf = new char[n];

Auto не спасет нас от целочисленного переполнения и для буфера будет выделено меньше памяти, чем 5ГиБ.

Авто также не очень помогает, когда дело доходит до очень распространенной ошибки: неправильно написанного цикла. Давайте посмотрим на пример:

std::vector<int> bigVector;
for (unsigned i = 0; i < bigVector.size(); ++i)
{ ... }

Для массивов большого размера этот цикл становится бесконечным. Неудивительно, что в коде есть такие ошибки: они проявляются в очень редких случаях, для которых не было тестов.

Можем ли мы переписать этот фрагмент с помощью auto?

std::vector<int> bigVector;
for (auto i = 0; i < bigVector.size(); ++i)
{ ... }

Нет. Не только ошибка все еще здесь. Стало еще хуже.

С простыми типами auto ведет себя очень плохо. Да, в простейших случаях (auto x = y) это работает, но как только появляются дополнительные конструкции, поведение может стать более непредсказуемым. Что еще хуже, ошибку будет труднее заметить, потому что типы переменных на первый взгляд не так очевидны. К счастью, для статических анализаторов это не проблема: они не устают и не теряют внимания. Но для нас, простых смертных, лучше явно указать типы. Избавиться от суживающего приведения можно и другими способами, но об этом мы поговорим позже.

Опасный счет

Один из «опасных» типов в C ++ - это массив. Часто, передавая его функции, программисты забывают, что он передается как указатель, и пытаются вычислить количество элементов с помощью sizeof.

#define RTL_NUMBER_OF_V1(A) (sizeof(A)/sizeof((A)[0]))
#define _ARRAYSIZE(A) RTL_NUMBER_OF_V1(A)
int GetAllNeighbors( const CCoreDispInfo *pDisp,
                     int iNeighbors[512] ) {
  ....
  if ( nNeighbors < _ARRAYSIZE( iNeighbors ) ) 
    iNeighbors[nNeighbors++] = pCorner->m_Neighbors[i];
  .... 
}

Примечание. Этот код взят из пакета SDK Source Engine.

Предупреждение PVS-Studio: V511 Оператор sizeof () возвращает размер указателя, а не массива в выражении sizeof (iNeighbors). Vrad_dll disp_vrad.cpp 60

Такая путаница может возникнуть из-за указания размера массива в аргументе: это число ничего не значит для компилятора и является лишь подсказкой для программиста.

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

template < class T, size_t N >
constexpr size_t countof( const T (&array)[N] ) {
  return N;
}
countof(iNeighbors); //compile-time error

Если мы перейдем к этой функции, а не к массиву, мы получим ошибку компиляции. В C ++ 17 вы можете использовать std :: size.

В C ++ 11 была добавлена ​​функция std :: extension , но она не подходит в качестве countof, поскольку возвращает 0 для неподходящих типов .

std::extent<decltype(iNeighbors)>(); //=> 0

Вы можете сделать ошибку не только с помощью countof,, но и с помощью sizeof .

VisitedLinkMaster::TableBuilder::TableBuilder(
    VisitedLinkMaster* master,
    const uint8 salt[LINK_SALT_LENGTH])
    : master_(master),
      success_(true) {
  fingerprints_.reserve(4096);
  memcpy(salt_, salt, sizeof(salt));
}

Примечание. Этот код взят из Chromium.

Предупреждения PVS-Studio:

  • V511 Оператор sizeof () возвращает размер указателя, а не массива, в выражении sizeof (salt). в браузере visitlink_master.cc 968
  • V512 Вызов функции memcpy приведет к опустошению буфера salt_. в браузере visitlink_master.cc 968

Как видите, у стандартных массивов C ++ много проблем. Вот почему вы должны использовать std :: array: в современном C ++ его API похож на std :: vector и другие контейнеры, и сложнее допустить ошибку, когда используй это.

void Foo(std::array<uint8, 16> array)
{
  array.size(); //=> 16
}

Как сделать ошибку в простом за

Еще один источник ошибок - простой цикл for. Вы можете подумать: «Где вы можете ошибиться? Это что-то связано со сложным условием выхода или экономией на строчках кода? » Нет, программисты ошибаются в простейших циклах. Давайте посмотрим на фрагменты из проектов:

const int SerialWindow::kBaudrates[] = { 50, 75, 110, .... };
SerialWindow::SerialWindow() : ....
{
  ....
  for(int i = sizeof(kBaudrates) / sizeof(char*); --i >= 0;)
  {
    message->AddInt32("baudrate", kBaudrateConstants[i]); 
    ....
  }
}

Примечание. Этот код взят из операционной системы Haiku.

Предупреждение PVS-Studio: V706 Подозрительное деление: sizeof (kBaudrates) / sizeof (char *). Размер каждого элемента в массиве kBaudrates не равен делителю. SerialWindow.cpp 162

Мы подробно рассмотрели такие ошибки в предыдущей главе: размер массива снова не был вычислен правильно. Мы можем легко исправить это, используя std :: size:

const int SerialWindow::kBaudrates[] = { 50, 75, 110, .... };
 
SerialWindow::SerialWindow() : ....
{
  ....
  for(int i = std::size(kBaudrates); --i >= 0;) {
    message->AddInt32("baudrate", kBaudrateConstants[i]); 
    ....
  }
}

Но есть способ получше. Взглянем еще на один фрагмент.

inline void CXmlReader::CXmlInputStream::UnsafePutCharsBack(
  const TCHAR* pChars, size_t nNumChars)
{
  if (nNumChars > 0)
  {
    for (size_t nCharPos = nNumChars - 1;
         nCharPos >= 0;
         --nCharPos)
      UnsafePutCharBack(pChars[nCharPos]);
  }
}

Примечание: этот код взят из Shareaza.

Предупреждение PVS-Studio: V547 Выражение nCharPos› = 0 всегда истинно. Значение беззнакового типа всегда ›= 0. BugTrap xmlreader.h 946

Типичная ошибка при написании обратного цикла: программист забыл, что итератор беззнакового типа и проверка всегда возвращают true. Вы можете подумать: «Как же так? Такие ошибки допускают только новички и студенты. Мы, профессионалы, этого не делаем ». К сожалению, это не совсем так. Конечно, все понимают, что (unsigned ›= 0) - true. Откуда такие ошибки? Часто они возникают в результате рефакторинга. Представьте себе такую ​​ситуацию: проект мигрирует с 32-битной платформы на 64-битную. Ранее для индексации использовалось int / unsigned, и было принято решение заменить их на size_t / ptrdiff_t. Но в одном фрагменте случайно вместо знакового был использован беззнаковый тип.

Что делать, чтобы избежать этой ситуации в вашем коде? Некоторые советуют использовать подписанные типы, как в C # или Qt. Возможно, это могло быть выходом из положения, но если мы хотим работать с большими объемами данных, то нет возможности избежать size_t. Есть ли более безопасный способ перебора массива в C ++? Есть конечно. Начнем с самого простого: функций, не являющихся членами. Есть стандартные функции для работы с коллекциями, массивами и initializer_list; их принцип должен быть вам знаком.

char buf[4] = { 'a', 'b', 'c', 'd' };
for (auto it = rbegin(buf);
     it != rend(buf);
     ++it) {
  std::cout << *it;
}

Отлично, теперь нам не нужно вспоминать разницу между прямым и обратным циклом. Также не нужно думать о том, используем ли мы простой массив или массив - цикл будет работать в любом случае. Использование итераторов - отличный способ избежать головной боли, но даже этого не всегда достаточно. Лучше всего использовать цикл for на основе диапазона:

char buf[4] = { 'a', 'b', 'c', 'd' };
for (auto it : buf) {
  std::cout << it;
}

Конечно, в range-based for нам не очень поможет. Но такие ситуации нужно рассматривать отдельно. У нас довольно простая ситуация: мы должны перемещаться по элементам в обратном порядке. Однако на данном этапе уже есть сложности. В стандартной библиотеке нет дополнительных классов для на основе диапазона для. Посмотрим, как это можно реализовать:

template <typename T>
struct reversed_wrapper {
  const T& _v;
 
  reversed_wrapper (const T& v) : _v(v) {}
  auto begin() -> decltype(rbegin(_v))
  {
    return rbegin(_v);
  }
  auto end() -> decltype(rend(_v))
  {
    return rend(_v);
  }
};
 
template <typename T>
reversed_wrapper<T> reversed(const T& v)
{
  return reversed_wrapper<T>(v);
}

В C ++ 14 можно упростить код, удалив decltype. Вы можете увидеть, как auto помогает писать шаблонные функции - reversed_wrapper будет работать как с массивом, так и с std :: vector.

Теперь мы можем переписать фрагмент следующим образом:

char buf[4] = { 'a', 'b', 'c', 'd' };
for (auto it : reversed(buf)) {
  std::cout << it;
}

Что хорошего в этом коде? Во-первых, его очень легко читать. Сразу видно, что массив элементов находится в обратном порядке. Во-вторых, ошибиться сложнее. И в-третьих, работает с любым типом. Это намного лучше, чем было.

Вы можете использовать boost :: adapters :: reverse (arr) в boost.

Но вернемся к исходному примеру. Там массив передается парой указатель-размер. Очевидно, что наша идея с перевернутым не сработает. Что нам следует сделать? Используйте такие классы, как span / array_view. В C ++ 17 есть string_view, и я предлагаю использовать это:

void Foo(std::string_view s);
std::string str = "abc";
Foo(std::string_view("abc", 3));
Foo("abc");
Foo(str);

string_view не владеет строкой, на самом деле это оболочка вокруг const char * и длины. Вот почему в примере кода строка передается по значению, а не по ссылке. Ключевой особенностью string_view является совместимость со строками в различных строковых представлениях: const char *, std :: string и без завершающего нуля const char *.

В результате функция принимает следующий вид:

inline void CXmlReader::CXmlInputStream::UnsafePutCharsBack(
  std::wstring_view chars)
{
  for (wchar_t ch : reversed(chars))
    UnsafePutCharBack(ch);
}

Переходя к функции, важно помнить, что конструктор string_view (const char *) неявный, поэтому мы можем написать так:

Foo(pChars);

Не так:

Foo(wstring_view(pChars, nNumChars));

Строка, на которую указывает string_view, не должна заканчиваться нулем, само имя string_view :: data дает нам подсказку об этом, и необходимо помните об этом при его использовании. При передаче его значения функции из cstdlib, которая ожидает строку C, вы можете получить неопределенное поведение. Вы можете легко пропустить это, если в большинстве случаев, которые вы тестируете, используются std :: string или строки с завершающим нулем.

Enum

Давайте на секунду оставим C ++ и подумаем о старом-добром C. Как там безопасность? В конце концов, нет проблем с неявными вызовами конструкторов и операторами или преобразованием типов, и нет проблем с различными типами строк. На практике часто возникают ошибки в простейших конструкциях: самые сложные тщательно проверяются и отлаживаются, так как вызывают некоторые сомнения. При этом программисты забывают проверять простые конструкции. Вот пример опасной структуры, пришедшей к нам из C:

enum iscsi_param {
  ....
  ISCSI_PARAM_CONN_PORT,
  ISCSI_PARAM_CONN_ADDRESS,
  ....
};
 
enum iscsi_host_param {
  ....
  ISCSI_HOST_PARAM_IPADDRESS,
  ....
};
int iscsi_conn_get_addr_param(....,
  enum iscsi_param param, ....)
{
  ....
  switch (param) {
  case ISCSI_PARAM_CONN_ADDRESS:
  case ISCSI_HOST_PARAM_IPADDRESS:
  ....
  }
 
  return len;
}

Пример ядра Linux. Предупреждение PVS-Studio: V556 Сравниваются значения разных типов перечислений: switch (ENUM_TYPE_A) {case ENUM_TYPE_B:…}. libiscsi.c 3501

Обратите внимание на значения в switch-case: одна из названных констант взята из другого перечисления. В оригинале, конечно, намного больше кода и больше возможных значений, и ошибка не так очевидна. Причина этого в слабой типизации enum - они могут неявно приводить к типу int, и это оставляет много места для ошибок.

В C ++ 11 вы можете и должны использовать enum class: такой трюк там не сработает, и ошибка появится на этапе компиляции. В результате следующий код не компилируется, что нам и нужно:

enum class ISCSI_PARAM {
  ....
  CONN_PORT,
  CONN_ADDRESS,
  ....
};
 
enum class ISCSI_HOST {
  ....
  PARAM_IPADDRESS,
  ....
};
int iscsi_conn_get_addr_param(....,
 ISCSI_PARAM param, ....)
{
  ....
  switch (param) {
  case ISCSI_PARAM::CONN_ADDRESS:
  case ISCSI_HOST::PARAM_IPADDRESS:
  ....
  }
 
  return len;
}

Следующий фрагмент не совсем связан с перечислением, но имеет схожие симптомы:

void adns__querysend_tcp(....) {
  ...
  if (!(errno == EAGAIN || EWOULDBLOCK || 
        errno == EINTR || errno == ENOSPC ||
        errno == ENOBUFS || errno == ENOMEM)) {
  ...
}

Примечание: этот код взят из ReactOS.

Да, значения errno объявляются как макросы, что является плохой практикой в ​​C ++ (в том числе и в C), но даже если бы программист использовал enum, он бы не делать жизнь легче. Потерянное сравнение не проявится в случае enum (и особенно в случае макроса). В то же время enum class не допустит этого, поскольку не будет неявного приведения к типу bool.

Инициализация в конструкторе

Но вернемся к родным проблемам C ++. Один из них выявляет, когда есть необходимость инициализировать объект одинаково в нескольких конструкторах. Простая ситуация: есть класс, два конструктора, один из них вызывает другой. Все выглядит довольно логично: общий код вынесен в отдельный метод - никто не любит дублировать код. В чем ошибка?

Guess::Guess() {
  language_str = DEFAULT_LANGUAGE;
  country_str = DEFAULT_COUNTRY;
  encoding_str = DEFAULT_ENCODING;
}
Guess::Guess(const char * guess_str) {
  Guess();
  ....
}

Примечание. Этот код взят из LibreOffice.

Предупреждение PVS-Studio: V603 Объект создан, но не используется. Если вы хотите вызвать конструктор, следует использовать this- ›Guess :: Guess (….). guess.cxx 56

Ошибка заключается в синтаксисе вызова конструктора. Часто об этом забывают, и программист создает еще один экземпляр класса, который затем немедленно уничтожается. То есть инициализация исходного экземпляра не происходит. Конечно, есть 1001 способ исправить это. Например, мы можем явно вызвать конструктор через this, или поместить все в отдельную функцию:

Guess::Guess(const char * guess_str)
{
  this->Guess();
  ....
}
 
Guess::Guess(const char * guess_str)
{
  Init();
  ....
}

Кстати, явный повторный вызов конструктора, например, через this - опасная игра, и нам нужно понимать, что происходит. Вариант с Init () намного лучше и понятнее. Тем, кто хочет лучше разобраться в деталях этих ловушек, я предлагаю взглянуть на главу 19 Как правильно вызывать один конструктор из другого из этой книги.

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

Guess::Guess(const char * guess_str) : Guess()
{
  ....
}

У таких конструкторов есть несколько ограничений. Первое: делегированные конструкторы берут на себя полную ответственность за инициализацию объекта. То есть невозможно будет инициализировать с его помощью другое поле класса в списке инициализации:

Guess::Guess(const char * guess_str)
  : Guess(),           
    m_member(42)
{
  ....
}

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

Guess::Guess(const char * guess_str)
  : Guess(std::string(guess_str))
{
  ....
}
Guess::Guess(std::string guess_str)
  : Guess(guess_str.c_str())
{
  ....
}

О виртуальных функциях

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

class Base {
  virtual void Foo(int x);
}
class Derived : public class Base {
  void Foo(int x, int a = 1);
}

Метод Derived :: Foo невозможно вызвать по указателю / ссылке на Base. Но это простой пример, и вы можете сказать, что никто не делает таких ошибок. Обычно люди ошибаются следующим образом:

Примечание. Этот код взят из MongoDB.

class DBClientBase : .... {
public:
  virtual auto_ptr<DBClientCursor> query(
    const string &ns,
    Query query,
    int nToReturn = 0
    int nToSkip = 0,
    const BSONObj *fieldsToReturn = 0,
    int queryOptions = 0,
    int batchSize = 0 );
};
class DBDirectClient : public DBClientBase {
public:
  virtual auto_ptr<DBClientCursor> query(
    const string &ns,
    Query query,
    int nToReturn = 0,
    int nToSkip = 0,
    const BSONObj *fieldsToReturn = 0,
    int queryOptions = 0);
};

Предупреждение PVS-Studio: V762 Рассмотрите возможность проверки аргументов виртуальной функции. См. Седьмой аргумент функции query в производном классе DBDirectClient и базовом классе DBClientBase. dbdirectclient.cpp 61

Аргументов много, и в функции наследника нет последнего аргумента. Это разные, не связанные между собой функции. Довольно часто такая ошибка возникает с аргументами, имеющими значение по умолчанию.

В следующем фрагменте ситуация немного сложнее. Этот код будет работать, если он скомпилирован как 32-битный код, но не будет работать в 64-битной версии. Первоначально в базовом классе параметр имел тип DWORD, но затем он был исправлен на DWORD_PTR. При этом в унаследованных классах это не изменилось. Да здравствует бессонная ночь, отладка и кофе!

class CWnd : public CCmdTarget {
  ....
  virtual void WinHelp(DWORD_PTR dwData, UINT nCmd = HELP_CONTEXT);
  ....
};
class CFrameWnd : public CWnd { .... };
class CFrameWndEx : public CFrameWnd {
  ....
  virtual void WinHelp(DWORD dwData, UINT nCmd = HELP_CONTEXT);
  ....
};

Ошибиться в подписи можно и более экстравагантными способами. Вы можете забыть о const функции или аргументе. Вы можете забыть, что функция в базовом классе не виртуальная. Вы можете перепутать тип подписанный / неподписанный.

В C ++ было добавлено несколько ключевых слов, которые могут регулировать переопределение виртуальных функций. Переопределение очень поможет. Этот код просто не компилируется.

class DBDirectClient : public DBClientBase {
public:
  virtual auto_ptr<DBClientCursor> query(
    const string &ns,
    Query query,
    int nToReturn = 0,
    int nToSkip = 0,
    const BSONObj *fieldsToReturn = 0,
    int queryOptions = 0) override;
};

NULL против nullptr

Использование NULL для указания нулевого указателя приводит к ряду непредвиденных ситуаций. Дело в том, что NULL - это обычный макрос, который расширяется до 0 и имеет тип int. Вот почему нетрудно понять, почему в этом примере выбрана вторая функция:

void Foo(int x, int y, const char *name);
void Foo(int x, int y, int ResourceID);
Foo(1, 2, NULL);

Хотя причина ясна, это очень нелогично. Вот почему существует потребность в nullptr с собственным типом nullptr_t. Вот почему мы не можем использовать NULL (и тем более 0) в современном C ++.

Другой пример: NULL можно использовать для сравнения с другими целочисленными типами. Предположим, есть функция WinAPI, которая возвращает HRESULT. Этот тип никак не связан с указателем, поэтому его сравнение с NULL бессмысленно. И nullptr подчеркивает это, выдавая ошибку компиляции, в то же время NULL работает:

if (WinApiFoo(a, b) != NULL)    // That's bad
if (WinApiFoo(a, b) != nullptr) // Hooray,
                                // a compilation error

va_arg

Бывают случаи, когда необходимо передать неопределенное количество аргументов. Типичный пример - функция форматированного ввода / вывода. Да, его можно написать так, что переменное количество аргументов не понадобится, но я не вижу причин отказываться от этого синтаксиса, потому что он намного удобнее и легче читается. Что предлагают старые стандарты C ++? Они предлагают использовать va_list. Какие у нас проблемы с этим? Не так-то просто передать аргумент неправильного типа такому аргументу. Или вообще не передавать аргумент. Рассмотрим фрагменты подробнее.

typedef std::wstring string16; 
const base::string16& relaunch_flags() const;
 
int RelaunchChrome(const DelegateExecuteOperation& operation)
{
  AtlTrace("Relaunching [%ls] with flags [%s]\n",
           operation.mutex().c_str(),
           operation.relaunch_flags());
  ....
}

Примечание. Этот код взят из Chromium.

Предупреждение PVS-Studio: V510 Не ожидается, что функция AtlTrace получит переменную типа класса в качестве третьего фактического аргумента. delegate_execute.cc 96

Программист хотел напечатать строку std :: wstring, но забыл вызвать метод c_str (). Таким образом, тип wstring будет интерпретироваться в функции как const wchar_t *. Конечно, толку от этого не будет.

cairo_status_t
_cairo_win32_print_gdi_error (const char *context)
{
  ....
  fwprintf (stderr, L"%s: %S", context,
            (wchar_t *)lpMsgBuf);
  ....
}

Примечание. Этот код взят из Каира.

Предупреждение PVS-Studio: V576 Неверный формат. Рассмотрите возможность проверки третьего фактического аргумента функции fwprintf. Ожидается указатель на строку символов типа wchar_t. cairo-win32-surface.c 130

В этом фрагменте программист перепутал спецификаторы формата строки. Дело в том, что в Visual C ++ wchar_t * и% S - char *, ждут wprintf% с. Интересно, что эти ошибки находятся в строках, которые предназначены для вывода ошибок или отладочной информации - конечно, это редкие случаи, поэтому они были пропущены.

static void GetNameForFile(
  const char* baseFileName,
  const uint32 fileIdx,
  char outputName[512] )
{
  assert(baseFileName != NULL);
  sprintf( outputName, "%s_%d", baseFileName, fileIdx );
}

Примечание. Этот код взят из CryEngine 3 SDK.

Предупреждение PVS-Studio: V576 Неверный формат. Рассмотрите возможность проверки четвертого фактического аргумента функции «sprintf». Ожидается аргумент целочисленного типа SIGNED. igame.h 66

Целочисленные типы тоже очень легко спутать. Особенно, если их размер зависит от платформы. Однако здесь все намного проще: перепутали подписанный и неподписанный типы. Большие числа будут печататься как отрицательные.

ReadAndDumpLargeSttb(cb,err)
  int     cb;
  int     err;
{
  ....
  printf("\n - %d strings were read, "
         "%d were expected (decimal numbers) -\n");
  ....
}

Примечание. Этот код взят из Word для Windows 1.1a.

Предупреждение PVS-Studio: V576 Неверный формат. При вызове функции printf ожидается другое количество фактических аргументов. Ожидается: 3. Присутствует: 1. dini.c 498

Пример найден при одном из археологических исследований. Эта строка предполагает три аргумента, но они не записаны. Возможно, программист намеревался распечатать данные в стеке, но мы не можем делать предположений о том, что там лежит. Конечно, нам нужно явно передать эти аргументы.

BOOL CALLBACK EnumPickIconResourceProc(
  HMODULE hModule, LPCWSTR lpszType, 
  LPWSTR lpszName, LONG_PTR lParam)
{
  ....
  swprintf(szName, L"%u", lpszName);
  ....
}

Примечание: этот код взят из ReactOS.

Предупреждение PVS-Studio: V576 Неверный формат. Рассмотрите возможность проверки третьего фактического аргумента функции «swprintf». Для вывода значения указателя необходимо использовать «% p». dialogs.cpp 66

Пример 64-битной ошибки. Размер указателя зависит от архитектуры, и использовать для него% u - плохая идея. Что мы будем использовать вместо этого? Анализатор подсказывает нам, что правильный ответ -% p. Хорошо, если указатель будет распечатан для отладки. Было бы намного интереснее, если бы потом была попытка прочитать его из буфера и использовать.

Что может быть не так с функциями с переменным количеством аргументов? Почти все! Вы не можете проверить тип аргумента или количество аргументов. Шаг влево, шаг вправо - неопределенное поведение.

Хорошо, что есть более надежные альтернативы. Во-первых, это вариативные шаблоны. С их помощью мы получаем всю информацию о переданных типах при компиляции, и можем использовать ее как захотим. В качестве примера возьмем тот самый printf, но более безопасный:

void printf(const char* s) {
  std::cout << s;
}
template<typename T, typename... Args>
void printf(const char* s, T value, Args... args) {
  while (s && *s) {
    if (*s=='%' && *++s!='%') {
      std::cout << value;
      return printf(++s, args...);
    }
    std::cout << *s++;
  }
}

Конечно, это всего лишь пример: на практике его использование бессмысленно. Но в случае вариативных шаблонов вы ограничены только вашим воображением, а не особенностями языка.

Еще одна конструкция, которая может использоваться как опция для передачи переменного количества аргументов - std :: initializer_list. Он не позволяет передавать аргументы разных типов. Но если этого достаточно, можно использовать:

void Foo(std::initializer_list<int> a);
Foo({1, 2, 3, 4, 5});

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

Сужение

Сужение приведений вызвало у программистов много головной боли. Особенно когда необходимость перехода на 64-битную архитектуру стала еще более острой. Очень хорошо, если в вашем коде есть только правильные типы. Но не все так хорошо: довольно часто программисты прибегают к различным грязным приемам и экстравагантным способам хранения указателей. Чтобы найти все такие фрагменты, потребовалось много кофе:

char* ptr = ...;
int n = (int)ptr;
....
ptr = (char*) n;

Но оставим на время тему 64-битных ошибок. Вот более простой пример: есть два целых числа, и программист хочет найти их соотношение. Делается это так:

virtual int GetMappingWidth( ) = 0;
virtual int GetMappingHeight( ) = 0;
 
void CDetailObjectSystem::LevelInitPreEntity()
{
  ....
  float flRatio = pMat->GetMappingWidth() /
                  pMat->GetMappingHeight();
  ....
}

Примечание. Этот код взят из пакета SDK Source Engine.

Предупреждение PVS-Studio: V636 Выражение было неявно приведено от типа int к типу float. Рассмотрите возможность использования явного приведения типа, чтобы избежать потери дробной части. Пример: double A = (double) (X) / Y ;. Клиент (HL2) detailobjectsystem.cpp 1480

К сожалению, защитить себя от таких ошибок невозможно - всегда будет еще один способ неявно преобразовать один тип в другой. Но хорошая новость заключается в том, что новый метод инициализации в C ++ 11 имеет одну приятную особенность: он запрещает сужающие приведения типов. В этом коде ошибка возникнет на этапе компиляции и ее легко исправить.

float flRatio { pMat->GetMappingWidth() /
                pMat->GetMappingHeight() };

Отсутствие новостей - хорошая новость

Есть множество способов ошибиться в управлении ресурсами и памятью. Удобство при работе - важное требование к современному языку. Современный C ++ не отстает и предлагает ряд инструментов для автоматического управления ресурсами. Хотя такие ошибки лежат в основе динамического анализа, некоторые проблемы можно выявить с помощью статического анализа. Вот некоторые из них:

void AccessibleContainsAccessible(....)
{
  auto_ptr<VARIANT> child_array(
           new VARIANT[child_count]);
  ...
}

Примечание. Этот код взят из Chromium.

Предупреждение PVS-Studio: V554 Некорректное использование auto_ptr. Память, выделенная с помощью new [], будет очищена с помощью delete. Interactive_ui_tests accessibility_win_browsertest.cc 171

Конечно, идея умных указателей не нова: например, был класс std :: auto_ptr. Я говорю об этом в прошедшем времени, потому что он был объявлен устаревшим в C ++ 11 и удален в C ++ 17. В этом фрагменте ошибка была вызвана некорректным использованием auto_ptr, класс не имеет специализации для массивов, в результате вместо него будет вызвано стандартное delete. a удалить []. unique_ptr заменил auto_ptr, и имеет специализацию для массивов и возможность передавать функтор deleter, который будет вызываться вместо delete, и полная поддержка семантики перемещения. Может показаться, что здесь ничего не может пойти не так.

void text_editor::_m_draw_string(....) const
{
  ....
  std::unique_ptr<unsigned> pxbuf_ptr(
       new unsigned[len]);
  ....
}

Примечание: этот код взят из nana.

Предупреждение PVS-Studio: V554 Некорректное использование unique_ptr. Память, выделенная с помощью «new []», будет очищена с помощью «delete». text_editor.cpp 3137

Оказывается, вы можете совершить точно такую ​​же ошибку. Да, достаточно было бы написать unique_ptr ‹unsigned []› и он исчезнет, ​​но тем не менее код компилируется и в этом виде. Значит, таким образом тоже можно ошибиться, и, как показывает практика, если возможно, то люди это делают. Этот фрагмент кода - тому подтверждение. Вот почему, используя unique_ptr с массивами, будьте предельно осторожны: выстрелить себе в ногу намного проще, чем кажется. Может быть, было бы лучше использовать std :: vector, как предписывает современный C ++?

Давайте посмотрим на еще один тип несчастного случая.

template<class TOpenGLStage>
static FString GetShaderStageSource(TOpenGLStage* Shader)
{
  ....
  ANSICHAR* Code = new ANSICHAR[Len + 1];
  glGetShaderSource(Shaders[i], Len + 1, &Len, Code);
  Source += Code;
  delete Code;
  ....
}

Примечание: этот код взят из Unreal Engine 4.

Предупреждение PVS-Studio: V611 Память была выделена оператором new T [], но освобождена оператором delete. Рассмотрите возможность проверки этого кода. Возможно, лучше использовать удалить [] код;. openglshaders.cpp 1790

Ту же ошибку можно легко сделать без интеллектуальных указателей: память, выделенная с помощью new [], освобождается с помощью delete.

bool CxImage::LayerCreate(int32_t position)
{
  ....
  CxImage** ptmp = new CxImage*[info.nNumLayers + 1];
  ....
  free(ptmp);
  ....
}

Примечание. Этот код взят из CxImage.

Предупреждение PVS-Studio: V611 Память была выделена оператором new, но освобождена функцией free. Рассмотрите возможность проверки логики работы за переменной «ptmp». ximalyr.cpp 50

В этом фрагменте перепутались malloc / free и new / delete. Это может произойти во время рефакторинга: были функции из C, которые нужно было заменить, и в результате у нас есть UB.

int settings_proc_language_packs(....)
{
  ....
  if(mem_files) {
    mem_files = 0;
    sys_mem_free(mem_files);
  }
  ....
}

Примечание: этот код взят из Fennec Media.

Предупреждение PVS-Studio: V575 Нулевой указатель передается в свободную функцию. Проверьте первый аргумент. настройки interface.c 3096

Это более забавный пример. Существует практика, когда указатель обнуляется после его освобождения. Иногда программисты даже пишут для этого специальные макросы. С одной стороны, это отличный метод: вы можете защитить себя от очередного выброса памяти. Но здесь порядок выражений запутался, и поэтому free получает нулевой указатель (который не ускользнул от внимания анализатора).

ETOOLS_API int __stdcall ogg_enc(....) {
  format = open_audio_file(in, &enc_opts);
  if (!format) {
    fclose(in);
    return 0;
  };
  out = fopen(out_fn, "wb");
  if (out == NULL) {
    fclose(out);
    return 0;
  }    
}

Но эта проблема касается не только управления памятью, но и управлением ресурсами. Например, вы забыли закрыть файл, как во фрагменте выше. И в обоих случаях ключевое слово-RAII. Эта же концепция лежит в основе интеллектуальных указателей. В сочетании с семантикой перемещения RAII помогает избежать множества ошибок, связанных с утечками памяти. Код, написанный в этом стиле, позволяет более наглядно идентифицировать владельца ресурса.

В качестве небольшого примера я предоставлю оболочку над FILE, которая использует возможности unique_ptr:

auto deleter = [](FILE* f) {fclose(f);};
std::unique_ptr<FILE, decltype(deleter)> p(fopen("1.txt", "w"),
                                           deleter);

Хотя вам, вероятно, может понадобиться более функциональная оболочка для работы с файлами (с более читаемым синтаксисом). Пора вспомнить, что в C ++ 17 будет добавлен API для работы с файловыми системами - std :: filesystem. Но если вас не устраивает это решение и вы хотите использовать fread / fwrite вместо потоков ввода-вывода, вы можете почерпнуть вдохновение из unique_ptr, и напишите свой собственный файл, который будет оптимизирован для ваших личных нужд, удобен, удобочитаем и безопасен.

Какой результат?

Современный C ++ предоставляет множество инструментов, которые помогут вам писать код более безопасно. Появилось множество конструкций для вычислений и проверок во время компиляции. Вы можете перейти на более удобную модель управления памятью и ресурсами.

Но не существует техники или парадигмы программирования, которые могли бы полностью защитить вас от ошибок. Вместе с функциональными возможностями C ++ также получает новые ошибки, которые будут свойственны только ему. Вот почему мы не можем полагаться исключительно на один метод: мы всегда должны использовать комбинацию проверки кода, качественного кода и достойных инструментов; которые могут помочь сэкономить ваше время и энергетические напитки, которые можно использовать более эффективно.

Говоря об инструментах, предлагаю попробовать PVS-Studio: мы недавно начали работать над Linux-версией, вы можете увидеть это в действии: он поддерживает любую систему сборки и позволяет вам проверить свой проект, просто собрав его. Для разработчиков Windows у нас есть удобный плагин для Visual Studio, который вы можете попробовать как пробную версию.