>
>
>
Статический анализ Си++ кода и новый ст…

Андрей Карпов
Статей: 674

Статический анализ Си++ кода и новый стандарт языка C++0x

В статье рассмотрены новые возможности языка Си++, описанные в стандарте C++0x и поддержанные в Visual Studio 2010. На примере PVS-Studio рассмотрено, как изменения языка отразятся на инструментах статического анализа кода.

Введение

Новый стандарт языка Си++ вот-вот придет в нашу жизнь. Пока его продолжают именовать C++0x, хотя, по всей видимости, его окончательное название - C++11. Новый стандарт уже частично поддерживается современными Си++ компиляторами, например Intel C++ и Visual C++. Поддержка далеко не полна, что вполне естественно. Во-первых, стандарт еще не принят, а во-вторых, даже когда он будет принят, потребуется время на проработку в компиляторах его особенностей.

Разработчики компиляторов не единственные, для кого важна поддержка нового стандарта. Нововведения языка оперативно должны быть поддержаны в инструментах статического анализа исходного кода. Новый стандарт обещает обратную совместимость. Почти гарантировано старый Си++ код будет корректно скомпилирован новым компилятором без необходимости каких-либо правок. Однако это не означает, что программа, не содержащая новые конструкции языка, сможет быть по-прежнему обработана статическим анализатором, не поддерживающим новый стандарт C++0x. Мы убедились в этом на практике, попытавшись проверить с помощью PVS-Studio проект, созданный еще в бета-версии Visual Studio 2010. Все дело в заголовочных файлах, в которых уже используются новые конструкции языка. Например, в заголовочном файле "stddef.h" можно увидеть использование нового оператора decltype:

namespace std { typedef decltype(__nullptr) nullptr_t; }

Естественно, что такие конструкции являются синтаксически неверными для анализатора, не поддерживающего C++0x, и приводят, либо к остановке его работы или неверным результатом. Стала очевидной необходимость поддержать C++0x в PVS-Studio к моменту выхода Visual Studio 2010, по крайней мере в том объеме, в котором новый стандарт поддерживается этим компилятором.

Можно заявить, что данная задача нами была успешно решена и на момент написания статьи, на сайте доступна версия PVS-Studio 3.50, интегрирующаяся как в Visual Studio 2005/2008, так и в Visual Studio 2010. Начиная с версии PVS-Studio 3.50 в инструменте реализована поддержка той части С++0x, которая реализована в Visual Studio 2010. Поддержка не идеальна, как например, при работе с "right-angle brackets", но мы продолжим работу по поддержке стандарта C++0x в следующих версиях.

В этой статье мы рассмотрим новые возможности языка, поддержка которых реализована в первой редакции Visual Studio 2010. При этом взглянем на эти возможности с различных позиций: что представляет из себя новая возможность, имеется ли связь с 64-битными ошибками, как новая конструкция языка была поддержана в PVS-Studio и как ее появление отразилось на библиотеке VivaCore.

Примечание. VivaCore - библиотека разбора, анализа и трансформации кода. VivaCore является открытой библиотекой и поддерживает языки Си и Си++. На основе VivaCore построен продукт PVS-Studio и на ее же основе могут быть созданы другие программные проекты.

Предлагаемую вашему вниманию статью можно назвать отчетом по исследованию и поддержке нового стандарта в PVS-Studio. Инструмент PVS-Studio диагностирует 64-битные и параллельные OpenMP ошибки. Но поскольку в данный момент более актуальной темой является переход на 64-битные системы, предпочтение будет отдано примерам, демонстрирующих обнаружение с помощью PVS-Studio 64-битных ошибок.

1. auto

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

for (vector<int>::iterator itr = myvec.begin(); 
     itr != myvec.end(); 
     ++itr)

Подобные конструкции весьма длинны и неудобны. Для сокращения записи можно использовать typedef, но это порождает новые сущности и мало добавляет с точки зрения удобства.

C++0x предлагает способ для смягчения этой проблемы. В новом стандарте значение ключевого слова auto будет заменено. Если раньше auto означало, что переменная создается в стеке, и подразумевалось неявно в случае, если вы не указали что-либо другое (register, к примеру), то теперь это аналог var в C# 3.0. Тип переменной, объявленной как auto, определяется компилятором самостоятельно на основе того, чем эта переменная инициализируется.

Следует заметить, что auto-переменная не сможет хранить значения разных типов в течение одного запуска программы. Си++ по-прежнему остается статически типизированным языком, и указание auto лишь говорит компилятору самостоятельно позаботиться об определении типа: после инициализации сменить тип переменной будет уже нельзя.

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

for (auto itr = myvec.begin(); itr != myvec.end(); ++itr)

Помимо удобства в написании кода и его упрощения, ключевое слово auto поможет сделать код более безопасным. Рассмотрим пример, где auto сделает код безопасным с точки зрения создания 64-битных приложений:

bool Find_Incorrect(const string *arrStr, size_t n)
{
  for (size_t i = 0; i != n; ++i)
  {
    unsigned n = arrStr[i].find("ABC");
    if (n != string::npos)
      return true;
  }
  return false;
};

Данный код содержит 64-битную ошибку. Функция корректно ведет себя при компиляции Win32 версии и дает сбой при сборке в режиме Win64. Ошибка заключается в использовании типа unsigned для переменной "n", хотя должен использоваться тип string::size_type, который возвращает функция find(). В 32-битной программе тип string::size_type и unsigned совпадают, и мы получаем корректные результаты. В 64-битной программе string::size_type и unsigned перестают совпадать. Когда подстрока не находится, функция find() возвращает значение string::npos, равное 0xFFFFFFFFFFFFFFFFui64. Это значение урезается до величины 0xFFFFFFFFu и помещается в 32-битную переменную. В результате условие 0xFFFFFFFFu != 0xFFFFFFFFFFFFFFFFui64 истинно и получается, что функция Find_Incorrect всегда возвращает true.

В данном примере ошибка не так страшна, так обнаруживается даже компилятором и тем более специализированным анализатором Viva64 (входящим в состав PVS-Studio).

Компилятор:

warning C4267: 'initializing' : 
conversion from 'size_t' to 'unsigned int', possible loss of data

Viva64:

V103: Implicit type conversion from memsize to 32-bit type.

Важнее то, что данная ошибка возможна и часто встречается в коде из-за неаккуратности при выборе типа для хранения возвращаемого значения. Возможно даже, что ошибка возникла из-за нежелания использовать громоздкую конструкцию вида string::size_type.

Теперь подобных ошибок легко избежать, при этом не загромождая код. Используя тип "auto" мы можем написать следующий простой и надежный код:

auto n = arrStr[i].find("ABC");
if (n != string::npos)
  return true;

Ошибка исчезла сама собой. Код не стал сложнее или менее эффективным. Вывод - использование "auto" рационально во многих случаях.

Ключевое слово "auto" сократит количество 64-битных ошибок или позволит исправить ошибки более изящно. Но само по себе использование "auto" вовсе не избавляет от всех 64-битных ошибок! Это всего лишь еще один инструмент языка, облегчающий жизнь программиста, но не делающий за него всю работу по контролю над типами. Рассмотрим пример:

void *AllocArray3D(int x, int y, int z,
                   size_t objectSize)
{
  int size = x * y * z * objectSize;
  return malloc(size);
}

Функция должна вычислить размер массива и выделить необходимое количество памяти. Логично ожидать, что в 64-битной среде эта функция сможет выделить память для работы с массивом размером 2000*2000*2000 типа "double". Однако вызов вида "AllocArray3D(2000, 2000, 2000, sizeof(double));" всегда будет возвращать NULL, как будто выделение такого объема памяти невозможно. Настоящей же причиной, по которой функция возвращает NULL, является ошибка переполнения в выражении "int size = x * y * z * sizeof(double)". Переменная "size" примет значение -424509440 и дальнейший вызов функции malloc не имеет смысла. Кстати, об опасности данного выражения предупредит и компилятор:

warning C4267: 'initializing' : 
conversion from 'size_t' to 'int', possible loss of data

Надеясь на "auto", неаккуратный программист может модифицировать код следующим образом:

void *AllocArray3D(int x, int y, int z,
                   size_t objectSize)
{
  auto size = x * y * z * objectSize;
  return (double *)malloc(size);
}

Однако это вовсе не устранит, а только замаскирует ошибку. Компилятор больше не выдаст предупреждение, но функция AllocArray3D по-прежнему будет возвращать NULL.

Тип переменной "size" автоматически станет "size_t". Но переполнение возникает при вычислении выражения "x * y * z". Это подвыражение имеет тип "int" и только затем тип будет расширен до "size_t" при умножении на переменную "objectSize".

Теперь эту спрятавшуюся ошибку можно будет обнаружить, только используя анализатор Viva64:

V104: Implicit type conversion to memsize type in an 
arithmetic expression.

Вывод - используя "auto", все-равно следует быть внимательным.

Теперь кратко рассмотрим, как новое ключевое слово было поддержано в библиотеке VivaCore, на которой и построен статический анализатор Viva64. Итак, анализатор должен уметь понять, что переменная AA имеет тип "int", чтобы, предупредить (см. V101) о расширении переменной АА до типа "size_t":

void Foo(int X, int Y)
{
  auto AA = X * Y;
  size_t BB = AA; //V101
}

Прежде всего, была составлена новая таблица лексем, которая включила новые ключевые слова C++0x. Эта таблица находится в файле Lex.cc и имеет имя tableC0xx. Для того чтобы не модифицировать старый код по обработке лексемы "auto" (tkAUTO), лексема "auto" в этой таблице имеет имя tkAUTOcpp0x.

В связи с появлением новой лексемы модификации подверглись следующие функции: isTypeToken, optIntegralTypeOrClassSpec. Появился новый класс LeafAUTOc0xx. В TypeInfoId появился новый класс объектов - AutoDecltypeType.

Для кодирования типа "auto" выбрана литера 'x', что нашло отражение в функциях классов TypeInfo и Encoding. Это, например, такие функции как IsAutoCpp0x, MakePtree.

Эти исправления позволяют разбирать код с ключевым "auto", имеющим новый смысл и сохранять тип объектов в закодированном виде (литера 'x'). Однако это не позволяет узнать, какой тип в действительности представляет переменная. То есть в VivaCore отсутствует функциональность, позволяющая узнать, что в выражении "auto AA = X * Y" переменная AA будет иметь тип "int".

Данная функциональность содержится в исходном коде Viva64 и не включается в состав кода библиотеки VivaCore. Принцип заключается в дополнительной работе по вычислению типа в методе TranslateAssignInitializer. После того, как вычислена правая часть выражения происходит подмена связи (Bind) имени переменной с типом.

2. decltype

В ряде случаев полезно "скопировать" тип некоторого объекта. Ключевое слово "auto" выводит тип, основываясь на выражении, используемом для инициализации переменной. Если инициализация отсутствует, то для определения типа выражения во время компиляции может быть использовано ключевое слово "decltype". Пример кода, где переменная "value" будет иметь тип, возвращаемый функцией "Calc()":

decltype(Calc()) value;
try {
  value = Calc(); 
}
catch(...) {
  throw;
}

Можно использовать "decltype" для объявления типа:

void f(const vector<int>& a,
       vector<float>& b)
{
  typedef decltype(a[0]*b[0]) Tmp;
  for (int i=0; i<b.size(); ++i)
  {
    Tmp* p = new Tmp(a[i]*b[i]);
    // ...
  }
}

Учтите, что тип, взятый с использованием decltype, может отличаться от типа, выведенного с помощью auto.

const std::vector<int> v(1);
auto a = v[0];decltype(v[0]) b = 1;
// тип a - int  
// тип b - const int& (возвращаемое значение
// std::vector<int>::operator[](size_type) const)

Перейдем к примеру, где "decltype" может быть полезен с точки зрения 64-битности. Функция IsPresent ищет элемент в последовательности и возвращает "true" если он найден:

bool IsPresent(char *array,
               size_t arraySize,
               char key)
{
  for (unsigned i = 0; i < arraySize; i++) 
    if (array[i] == key)
      return true;
  return false;
}

Данная функция неспособна работать в 64-битной системе с большими массивами. Если переменная arraySize будет иметь значение больше UINT_MAX, то условие "i < arraySize" никогда не выполнится и возникнет вечный цикл.

Если мы воспользуемся ключевым словом "auto", то это ничего не изменит:

for (auto i = 0; i < arraySize; i++) 
  if (array[i] == key)
    return true;

Переменная "i" будет иметь тип "int", так как 0 имеет тип "int". Правильным исправлением может стать использование "decltype":

for (decltype(arraySize) i = 0; i < arraySize; i++) 
  if (array[i] == key)
    return true;

Теперь счетчик "i" имеет тип "size_t" как и переменная "arraySize".

Поддержка "decltype" в библиотеке VivaCore во многом схожа с поддержкой "auto". Добавлена новая лексема tkDECLTYPE. Добавлена функция парсинга rDecltype в файле Parser.cc. В связи с появлением новой лексемы модификации подверглась функция optIntegralTypeOrClassSpec. Появился новый класс LeafDECLTYPE.

Для кодирования типа возвращаемого оператором "decltype" выбрана литера 'X' (заглавная буква 'X', в отличие от прописной 'x', используемой для auto). В связи с этим изменилась функциональность классов TypeInfo и Encoding. Например, функции WhatIs, IsDecltype, MakePtree.

Функциональность по вычислению типов для оператора "decltype" реализована в классе Environment и входит в состав библиотеки VivaCore. Вычисление типа осуществляется в момент записи новой переменной/типа в Environment (функции RecordTypedefName, RecordDeclarator, RecordConstantDeclarator). За вычисление типа отвечает функция FixIfDecltype.

3. Ссылка на временный объект (R-value reference)

В стандарте C++98 временные объекты можно передавать в функции, но только как константную ссылку (const &). Следовательно, функция не в состоянии определить, временный это объект или нормальный, который тоже передали как const &.

В C++0x будет добавлен новый тип ссылки — ссылка на временный объект (R-value reference). Его объявление следующее: "ИМЯ_ТИПА &&". Оно может быть использовано как не константный, легально модифицируемый объект. Данное нововведение позволяет учитывать временные объекты и реализовывать семантику переноса (Move semantics). Например, если std::vector создается как временный объект или возвращается из функции — можно, создавая новый объект, просто перенести все внутренние данные из ссылки нового типа. Конструктор переноса std::vector через полученную ссылку на временный объект просто копирует указатель массива, находящийся в ссылке, которая по окончании устанавливается в пустое состояние.

Конструктор переноса или оператор переноса может быть объявлен следующим образом:

template<class T> class vector {
  // ...
  vector(const vector&); // copy constructor
  vector(vector&&);      // move constructor
  vector& operator=(const vector&); // copy assignment
  vector& operator=(vector&&);      // move assignment
};

С точки зрения анализа 64-битных ошибок в коде для нас не имеет значения, обрабатывается при объявлении типа '&' или '&&'. Соответственно поддержка данного нововведения в VivaCore весьма проста. Изменения затронули только функцию optPtrOperator класса Parser. В ней мы равнозначно воспринимаем как '&', так и '&&'.

4. Правые угловые скобки

С точки зрения стандарта C++98 следующая конструкция содержит синтаксическую ошибку:

list<vector<string>> lvs;

Для ее предотвращения необходимо вставить пробел между двумя правыми закрывающимися угловыми скобками:

list<vector<string> > lvs;

Стандарт С++0x узаконил использование двойных закрывающихся скобок при объявлении шаблонных типов, без необходимости вставлять между ними пробел. В результате становится возможным написание чуть более элегантного кода.

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

На данный момент, разбор объявлений шаблонных типов с ">>" пока реализован в VivaCore не лучшим образом. В ряде случаев анализатор ошибается и видимо со временем части анализатора, связанные с разбором шаблонов будут нами существенно переработаны. Пока в коде можно увидеть следующие некрасивые функции, которые эвристическими методами пытаются определить, имеем мы дело с оператором сдвига ">>" или с частью объявления шаблонного типа "A<B<C>> D": IsTemplateAngleBrackets, isTemplateArgs. Тем, кому интересно, как корректно подойти к решению данной задачи, будет полезен следующий документ: "Right Angle Brackets (N1757)". Со временем мы улучшим обработку правых угловых скобок в VivaCore.

5. Лямбда-функции (Lambdas)

Лямбда-выражения в Си++ — это краткая форма записи анонимных функторов (объектов, которые можно использовать как функцию). Рассмотрим немного историю. В Си для создания функторов используются указатели на функцию:

/* callback-функция */
int compare_function(int A, int B) {
  return A < B;
}
 
/* объявление функции сортировки */
void mysort(int* begin_items,
            int num_items,
            int (*cmpfunc)(int, int));
 
int main(void) {
    int items[] = {4, 3, 1, 2};
    mysort(items,
           sizeof(items)/sizeof(int),
           compare_function);
    return 0;
}

Ранее в Си++ функтор создавали с помощью класса, у которого перегружен operator():

class compare_class {
  public:
  bool operator()(int A, int B) {
    return (A < B);
  }
};
        
// объявление функции сортировки
template <class ComparisonFunctor> 
void mysort (int* begin_items,
             int num_items,
             ComparisonFunctor c);
 
int main() {
    int items[] = {4, 3, 1, 2};
    compare_class functor;
    mysort(items,
    sizeof(items)/sizeof(int),
    functor);
}

В C++0x мы получаем возможность объявить функтор еще более элегантно:

auto compare_function = [](char a, char b)
  { return a < b; };
char Str[] = "cwgaopzq";
std::sort(Str,
          Str + strlen(Str),
          compare_function);
cout << Str << endl;

Мы заводим переменную compare_function которая является функтором и тип которой определяется компилятором автоматически. Затем мы может передать эту переменную в std::sort. Мы можем еще более сократить код:

char Str[] = "cwgaopzq";
std::sort(
  Str,
  Str + strlen(Str),
  [](char a, char b) {return a < b;}
);
cout << Str << endl;

Здесь "[](char a, char b) {return a < b;}" и есть не что иное, как лямбда-функция.

Лямбда-выражение всегда начинается со скобок [], в которых может быть указан список захвата. Затем идет необязательный список параметров и необязательный тип возвращаемого значения. Завершает объявление непосредственно тело функции. В целом формат написания лямбда функций следующий:

'[' [<список_захвата>] ']'
[ '(' <список_параметров> ')' ['mutable' ] ]
[ 'throw' '(' [<типы_исключений>] ')' ]
[ '->' <тип_возвращаемого_значения> ]
'{' [<тело_функции>] '}'

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

Список захвата указывает, к каким объектам из внешней области видимости имеет доступ лямбда-функция:

  • [] - без захвата переменных из внешней области видимости;
  • [=] - все переменные захватываются по значению;
  • [&] - все переменные захватываются по ссылке;
  • [x, y] - захват x и y по значению;
  • [&x, &y] - захват x и y по ссылке;
  • [in, &out] - захват in по значению, а out — по ссылке;
  • [=, &out1, &out2] - захват всех переменных по значению, кроме out1 и out2, которые захватываются по ссылке;
  • [&, x, &y] - захват всех переменных по ссылке, кроме x.

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

Программа создает массив строк и массив индексов. Затем программа сортирует индексы строк таким образом, что бы строки были расположены по увеличению их длины:

int _tmain(int, _TCHAR*[])
{
  vector<string> strings;
  strings.push_back("lambdas");
  strings.push_back("decltype");
  strings.push_back("auto");
  strings.push_back("static_assert");
  strings.push_back("nullptr");
  vector<size_t> indices;
  size_t k = 0;
  generate_n(back_inserter(indices),
             strings.size(),
             [&k]() { return k++; });
  sort(indices.begin(),
       indices.end(),
       [&](ptrdiff_t i1, ptrdiff_t i2)
       { return strings[i1].length() <
                strings[i2].length(); });
  for_each(indices.begin(),
           indices.end(),
           [&strings](const size_t i)
           { cout << strings[i] << endl; });
  return 0;
}

Примечание.Согласно С++0x можно инициализировать массивы std::vector следующим образом:

vector<size_t> indices = {0,1,2,3,4};

Но пока Visual Studio 2010 не поддерживает подобные конструкции.

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

В PVS-Studio реализована полноценная диагностика ошибок в лямбда-функциях. Рассмотрим пример кода, содержащий 64-битную ошибку:

int a = -1;
unsigned b = 0;
const char str[] = "Viva64";
const char *p = str + 1;
auto lambdaFoo = [&]() -> char
  {
    return p[a+b];
  };
cout << lambdaFoo() << endl;

Данный код работает при компиляции в режиме Win32 и печатает на экран букву 'V'. В режиме Win64 программа аварийно завершается из-за попытки обращению к элементу с номером 0xFFFFFFFF. Подробнее о данном виде ошибок рассказано в уроках по разработке 64-битных приложений на языке Си/Си++ - " Урок 13. Паттерн 5. Адресная арифметика".

При проверке приведенного выше кода, PVS-Studio выводит диагностическое сообщение:

error V108: Incorrect index type: p[not a memsize-type]. Use memsize
  type instead.

Соответственно анализатор должен был для этого разобрать лямбда-функцию и разобраться с областью видимости переменных. Непростая, но необходимая функциональность.

С поддержкой лямбда-функций связаны самые значительные изменения в VivaCore. В процессе построения дерева разбора теперь участвует новая функция rLambdas. Функция находится в классе Parser и вызывается из таких функций как rInitializeExpr, rFunctionArguments, rCommaExpression. Функция rLambdas разбирает лямбда-функции и добавляет в дерево новый тип объекта - PtreeLambda. Класс PtreeLambda объвлен и реализован в файлах PtreeLambda.h и PtreeLambda.

Обработку PtreeLambda в построенном дереве осуществляет функция TranslateLambda. Вся логика по работе с лямда-функциями сосредоточена в VivaCore. Внутри TranslateLambda вы встретите вызов функции GetReturnLambdaFunctionTypeForReturn, реализованную в коде PVS-Studio. Но данная функция служит для внутренних целей PVS-Studio и пустая заглушка-функция GetReturnLambdaFunctionTypeForReturn в VivaCore никак не скажется на разборе кода.

6. Suffix return type syntax

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

template<class T, class U>
??? mul(T x, U y)
{
  return x*y;
}

Возвращаемый тип должен является типом выражения "x*y". Но, непонятно, что можно написать вместо "???". Первой идеей может быть использование "decltype":

template<class T, class U>
decltype(x*y) mul(T x, U y) //Scope problem!
{
  return x*y;
}

Переменные "x" и "y" объявлены после "decltype(x*y)" и такой код, к сожалению, не может быть скомпилирован.

Решение данной проблемы заключается в использовании нового синтаксиса возвращаемых значений:

template<class T, class U>
[] mul(T x, U y) -> decltype(x*y)
{
  return x*y;
}

Используя скобки [] мы порождаем здесь лямда-функцию и при этом говорим "возвращаемый тип будет выведен или задан позже". К сожалению, хотя приведенный пример корректен, он на момент написания статьи не компилируется в Visual C++. Однако, мы можем использовать альтернативный вариант (где также используется Suffix return type syntax):

template<class T, class U>
auto mul(T x, U y) -> decltype(x*y)
{
  return x*y;
}

Этот код будет успешно собран Visual C++ и мы получим желаемый результат.

В версии PVS-Studio 3.50 поддержка нового формата функций реализована только частично. Конструкции полностью разбираются библиотекой VivaCore, но PVS-Studio при анализе не учитывает типы данных, возвращаемые этими функциями. Познакомиться с поддержкой альтернативной записи функций в библиотеке VivaCore можно в функции Parser::rIntegralDeclaration.

7. static_assert

В стандарте C++0x появилось новое ключевое слово static_assert. Синтаксис:

static_assert(выражение, "сообщение об ошибке");

В случае если выражение ложно, то выводится указанное сообщение об ошибке и компиляция прекращается. Рассмотрим пример использования static_assert:

template <unsigned n>
struct MyStruct
{
  static_assert(n > 5, "N must be more 5");
};
MyStruct<3> obj;

При компиляции данного кода компилятор Visual C++ выдаст сообщение:

error C2338: N must be more 5
  xx.cpp(33) : see reference to class template
  instantiation 'MyStruct<n>' being compiled 
  with
  [
    n=3
  ]

С точки зрения анализа кода, осуществляемого PVS-Studio, конструкция static_assert не представляет интереса и поэтому игнорируется. В VivaCore добавлена новая лексема tkSTATIC_ASSERT. Встречая эту лексему, лексер игнорирует ее и все параметры, относящиеся к конструкции static_assert (реализация в функции Lex::ReadToken).

8. nullptr

До стандарта C++0x в Си++ не было ключевого слова для обозначения нулевого указателя. Для его обозначения использовалось число 0. Однако хорошим стилем считалось использование макроса NULL. При раскрытии макрос NULL превращается в 0, и между ними нет практической разницы. Вот как объявлен макрос NULL в Visual Studio:

#define NULL    0

В ряде случаев отсутствие специального ключевого слова для обозначения нулевого указателя было неудобно и даже провоцировало возникновение ошибок. Рассмотрим пример:

void Foo(int a)
{ cout << "Foo(int a)" << endl; }
void Foo(char *a)
{ cout << "Foo(char *a)" << endl; }
int _tmain(int, _TCHAR*[])
{
  Foo(0);
  Foo(NULL);
  return 0;
}

Хотя программист может ожидать, что в данном коде будут вызваны разные функции Foo, это не так. Вместо NULL будет подставлен 0, имеющий тип "int" и при запуске программы на экране будет распечатано:

Foo(int a)
Foo(int a)

Для устранения подобных ситуаций в C++0x введено ключевое слово nullptr. Константа nullptr имеет тип nullptr_t и неявно приводится к любому типу указателя или к указателю на члены класса. Константа nullptr неявно не приводится к целочисленным типам данных за исключением типа "bool".

Вернемся к нашему примеру и добавим вызов функции "Foo" с аргументом nullptr:

void Foo(int a)
{ cout << "Foo(int a)" << endl; }
void Foo(char *a)
{ cout << "Foo(char *a)" << endl; }
int _tmain(int, _TCHAR*[])
{
  Foo(0);
  Foo(NULL);
  Foo(nullptr);
  return 0;
}

Теперь на экране будет распечатано:

Foo(int a)
Foo(int a)
Foo(char *a)

Хотя ключевое слово nullptr не представляет интереса с точки зрения поиска 64-битных ошибок, необходима его поддержка при разборе кода. Для этого в VivaCore был добавлена новая лексема tkNULLPTR, а также класс LeafNULLPTR. Создание объектов типа LeafNULLPTR происходит в функции rPrimaryExpr. При вызове функции LeafNULLPTR::Typeof тип "nullptr" кодируется как "Pv", то-есть "void *". С точки зрения существующих задач по анализу кода в PVS-Studio этого достаточно.

9. Новые стандартные классы

Стандарт C++0x вводит новые стандартные классы, относящиеся к namespace std. Ряд из этих классов уже поддерживаются в Visaul Studio 2010. В качестве примера можно привести:

  • std::array;
  • std::shared_ptr;
  • std::regex.

Поскольку перечисленные сущности являются обыкновенными шаблонными классами, то их появление не потребовало какой либо модификации PVS-Studio или библиотеки VivaCore.

10. Новые направления в развитии статических анализаторов кода

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

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

{
  int x;
  std::vector<int> A(10);
  A[0] = x; // Warning C4700
}
{
  int x;
  std::vector<int> A(10);
  std::for_each(A.begin(), A.end(),
    [x](int &y)
    { y = x; } // No Warning
  );
}

Программист может надеяться получить предупреждение от компилятора как в первом, так и во втором случае. Но в примере с лямбда-функцией никакого предупреждения выдано не будет (испытано на Visual Studio 2010 RC, /W4). Как не было ранее и многих других предупреждений на различные опасные ситуации. Требуется время на реализацию подробной диагностики.

Можно ожидать новый виток развития статических анализаторов, с точки зрения поиска потенциально опасных конструкций, которые возникают при использовании конструкций C++0x. Мы позиционируем наш продукт PVS-Studio как средство для проверки современных программ. В настоящий момент мы понимаем под этим 64-битные и параллельные технологии. В будущем мы планируем провести исследования вопроса о том, какие потенциальные проблемы можно ожидать при использовании C++0x. Если подводных камней будет достаточно много, то возможно мы приступим к созданию нового инструмента для их диагностики.

Заключение

На наш взгляд C++0x привносит много положительных моментов. Старый код не требует немедленной модернизации, хотя может быть со временем модифицирован в ходе рефакторинга. Новый же код может уже писаться с использованием новых конструкций. Таким образом, начало использования C++0x выглядит рациональным уже сейчас.

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