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

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

Введение1. auto2. decltype3. Ссылка на временный объект (R-value reference)4. Правые угловые скобки5. Лямбда-функции (Lambdas)6. Suffix return type syntax7. static_assert8. nullptr9. Новые стандартные классы10. Новые направления в развитии статических анализаторов кодаЗаключениеБиблиографический список

 

Введение

Новый стандарт языка Си++ вот-вот придет в нашу жизнь. Пока его продолжают именовать 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::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& a, vector& b){ typedef decltype(a[0]*b[0]) Tmp; for (int i=0; i>" пока реализован в VivaCore не лучшим образом. В ряде случаев анализатор ошибается и видимо со временем части анализатора, связанные с разбором шаблонов будут нами существенно переработаны. Пока в коде можно увидеть следующие некрасивые функции, которые эвристическими методами пытаются определить, имеем мы дело с оператором сдвига ">>" или с частью объявления шаблонного типа "A 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 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

Мой блог находят по следующим фразам

Данная статья "Статический анализ Си++ кода и новый стандарт языка C++0x" размещена на сайте Компьютерные сети и многоуровневая архитектура интернета (conlex.kz) в ознакомительных целях.

Уточнения, корректировки и обсуждения статьи "Статический анализ Си++ кода и новый стандарт языка C++0x" - под данным текстом, в комментариях.

Ответственность, за все изменения, внесённые в систему по советам данной статьи, Вы берёте на себя.

Копирование статьи "Статический анализ Си++ кода и новый стандарт языка C++0x", без указания ссылки на сайт первоисточника Компьютерные сети и многоуровневая архитектура интернета (conlex.kz), строго запрещено.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *