Введение
1. auto
2. decltype
3. Ссылка на временный объект (R-value reference)
4. Правые угловые скобки
5. Лямбда-функции (Lambdas)
6. Suffix return type syntax
7. static_assert
8. nullptr
9. Новые стандартные классы
10. Новые направления в развитии статических анализаторов кода
Заключение
Библиографический список
Аннотация
В статье рассмотрены новые возможности языка Си++, описанные в стандарте 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; } |
Можно заявить, что данная задача нами была успешно решена и на момент написания статьи, на сайте доступна версия 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) |
C++0x предлагает способ для смягчения этой проблемы. В новом стандарте значение ключевого слова auto будет заменено. Если раньше auto означало, что переменная создается в стеке, и подразумевалось неявно в случае, если вы не указали что-либо другое (register, к примеру), то теперь это аналог var в C# 3.0. Тип переменной, объявленной как auto, определяется компилятором самостоятельно на основе того, чем эта переменная инициализируется.
Следует заметить, что auto-переменная не сможет хранить значения разных типов в течение одного запуска программы. Си++ по прежнему остается статически типизированным языком, и указание auto лишь говорит компилятору самостоятельно позаботиться об определении типа: после инициализации сменить тип переменной будет уже нельзя.
Теперь итератор может быть объявлен следующим образом:
for (auto itr = myvec.begin(); itr != myvec.end(); ++itr) |
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;
}; |
В данном примере ошибка не так страшна, так обнаруживается даже компилятором и тем более специализированным анализатором Viva64 (входящим в состав PVS-Studio).
Компилятор:
warning C4267: 'initializing' : conversion from 'size_t' to 'unsigned int', possible loss of data |
V103: Implicit type conversion from memsize to 32-bit type. |
Теперь подобных ошибок легко избежать, при этом не загромождая код. Используя тип «auto» мы можем написать следующий простой и надежный код:
auto n = arrStr[i].find("ABC");
if (n != string::npos)
return true; |
Ключевое слово «auto» сократит количество 64-битных ошибок или позволит исправить ошибки более изящно. Но само по себе использование «auto» вовсе не избавляет от всех 64-битных ошибок! Это всего лишь еще один инструмент языка, облегчающий жизнь программиста, но не делающий за него всю работу по контролю над типами. Рассмотрим пример:
void *AllocArray3D(int x, int y, int z,
size_t objectSize)
{
int size = x * y * z * objectSize;
return malloc(size);
} |
warning C4267: 'initializing' : conversion from 'size_t' to 'int', possible loss of data |
void *AllocArray3D(int x, int y, int z,
size_t objectSize)
{
auto size = x * y * z * objectSize;
return (double *)malloc(size);
} |
Тип переменной «size» автоматически станет "size_t". Но переполнение возникает при вычислении выражения «x * y * z». Это подвыражение имеет тип «int» и только затем тип будет расширен до «size_t» при умножении на переменную «objectSize».
Теперь эту спрятавшуюся ошибку можно будет обнаружить, только используя анализатор Viva64:
V104: Implicit type conversion to memsize type in an arithmetic expression. |
Теперь кратко рассмотрим, как новое ключевое слово было поддержано в библиотеке VivaCore, на которой и построен статический анализатор Viva64. Итак, анализатор должен уметь понять, что переменная AA имеет тип «int», чтобы, предупредить (см. V101) о расширении переменной АА до типа «size_t»:
void Foo(int X, int Y)
{
auto AA = X * Y;
size_t BB = AA; //V101
} |
В связи с появлением новой лексемы модификации подверглись следующие функции: 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;
} |
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]);
// ...
}
} |
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) |
bool IsPresent(char *array,
size_t arraySize,
char key)
{
for (unsigned i = 0; i < arraySize; i++)
if (array[i] == key)
return true;
return false;
} |
Если мы воспользуемся ключевым словом «auto», то это ничего не изменит:
for (auto i = 0; i < arraySize; i++)
if (array[i] == key)
return true; |
for (decltype(arraySize) i = 0; i < arraySize; i++)
if (array[i] == key)
return true; |
Поддержка «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; |
Поддержку данного нововведения важно реализовать в статическом анализаторе, так как разработчики с удовольствием перестанут писать лишние пробелы.
На данный момент, разбор объявлений шаблонных типов с ">>" пока реализован в 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;
} |
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);
} |
auto compare_function = [](char a, char b)
{ return a < b; };
char Str[] = "cwgaopzq";
std::sort(Str,
Str + strlen(Str),
compare_function);
cout << Str << endl; |
char Str[] = "cwgaopzq";
std::sort(
Str,
Str + strlen(Str),
[](char a, char b) {return a < b;}
);
cout << Str << endl; |
Лямбда-выражение всегда начинается со скобок [], в которых может быть указан список захвата. Затем идет необязательный список параметров и необязательный тип возвращаемого значения. Завершает объявление непосредственно тело функции. В целом формат написания лямбда функций следующий:
'[' [<список_захвата>] ']'
[ '(' <список_параметров> ')' ['mutable' ] ]
[ 'throw' '(' [<типы_исключений>] ')' ]
[ '->' <тип_возвращаемого_значения> ]
'{' [<тело_функции>] '}' |
- [] — без захвата переменных из внешней области видимости;
- [=] — все переменные захватываются по значению;
- [&] — все переменные захватываются по ссылке;
- [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;
} |
vector<size_t> indices = {0,1,2,3,4}; |
Качество анализа лямбда-функций в статических анализаторах должно соответствовать качеству анализа простых функций. В целом анализ лямбда-функций схож с анализом простых функций, за исключением того, что лямбда-функции имеют другую область видимости.
В 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; |
При проверке приведенного выше кода, 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;
} |
template<class T, class U>
decltype(x*y) mul(T x, U y) //Scope problem!
{
return x*y;
} |
Решение данной проблемы заключается в использовании нового синтаксиса возвращаемых значений:
template<class T, class U>
[] mul(T x, U y) -> decltype(x*y)
{
return x*y;
} |
template<class T, class U>
auto mul(T x, U y) -> decltype(x*y)
{
return x*y;
} |
В версии PVS-Studio 3.50 поддержка нового формата функций реализована только частично. Конструкции полностью разбираются библиотекой VivaCore, но PVS-Studio при анализе не учитывает типы данных, возвращаемые этими функциями. Познакомиться с поддержкой альтернативной записи функций в библиотеке VivaCore можно в функции Parser::rIntegralDeclaration.
7. static_assert
В стандарте C++0x появилось новое ключевое слово static_assert. Синтаксис:
static_assert(выражение, "сообщение об ошибке"); |
template <unsigned n>
struct MyStruct
{
static_assert(n > 5, "N must be more 5");
};
MyStruct<3> obj; |
error C2338: N must be more 5
xx.cpp(33) : see reference to class template
instantiation 'MyStruct<n>' being compiled
with
[
n=3
] |
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(int a) Foo(int a) |
Вернемся к нашему примеру и добавим вызов функции «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) |
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
);
} |
Можно ожидать новый виток развития статических анализаторов, с точки зрения поиска потенциально опасных конструкций, которые возникают при использовании конструкций C++0x. Мы позиционируем наш продукт PVS-Studio как средство для проверки современных программ. В настоящий момент мы понимаем под этим 64-битные и параллельные технологии. В будущем мы планируем провести исследования вопроса о том, какие потенциальные проблемы можно ожидать при использовании C++0x. Если подводных камней будет достаточно много, то возможно мы приступим к созданию нового инструмента для их диагностики.
Заключение
На наш взгляд C++0x привносит много положительных моментов. Старый код не требует немедленной модернизации, хотя может быть со временем модифицирован в ходе рефакторинга. Новый же код может уже писаться с использованием новых конструкций. Таким образом, начало использования C++0x выглядит рациональным уже сейчас.
Библиографический список
- Bjarne Stroustrup. C++0x — the next ISO C++ standard. http://www.viva64.com/go.php?url=304
- Visual C++ Team Blog. Rvalue References: C++0x Features in VC10, Part 2. http://www.viva64.com/go.php?url=305
- Сергей Олендаренко. C++0x. Лямбда-выражения. http://www.viva64.com/go.php?url=306
- Максим. С++0x и решение проблем инициализации. http://www.viva64.com/go.php?url=307
- Wikipedia. C++0x. http://www.viva64.com/go.php?url=301
