В лучшем случае такой код даст сбой при тестировании на машине разработчика, а не на машине клиента. Однако довольно часто код на первый взгляд работает, но при этом в некоторых ситуациях в нем разыменовываются висящие указатели, итераторы и т.д. В таких случаях мы хотим, чтобы нас оповестили, если мы напишем код, демонстрирующий неопределенное поведение.
К счастью, спасение есть! Реализация GNU STL имеет режим отладки, а компиляторы GNU C++ и LLVM clang C++ поддерживают дополнительные библиотеки, пригодные для создания сверхчувствительных и избыточных бинарных файлов, которые будут мгновенно сообщать о большинстве ошибок. Их легко использовать, и они очень полезны, что мы и продемонстрируем в этом разделе. Стандартная библиотека Microsoft Visual C++ также предоставляет возможность проведения дополнительных проверок.
Как это делается
В этом примере мы напишем программу, которая намеренно получает доступ к некорректному итератору.
1. Сначала включим заголовочные файлы:
#include
#include
2. Теперь создадим вектор, содержащий целые числа, и получим итератор, указывающий на первый элемент, — значение
1
. Мы применим функцию shrink_to_fit()
для вектора с целью убедиться, что его емкость действительно равна 3
, поскольку данная реализация может выделять фрагмент памяти больше необходимого, чтобы благодаря этому небольшому резерву будущие операции вставки проходили быстрее:
int main()
{
std::vector v {1, 2, 3};
v.shrink_to_fit();
const auto it (std::begin(v));
3. Далее выведем на экран разыменованный итератор, что совершенно корректно:
std::cout << *it << '\n';
4. Добавим в вектор новое число. Поскольку он недостаточно велик, чтобы свободно принять новое число, он автоматически увеличится в размере. Это достигается за счет выделения более крупного фрагмента памяти, перемещения всех существующих элементов в новый фрагмент и удаления старого.
v.push_back(123);
5. Теперь снова выведем на экран значение
1
из вектора с помощью данного итератора. Это кончится плохо. Почему? Когда вектор переместил все свои значения в новый фрагмент памяти и удалил старый, он не сообщил итератору о текущем изменении. Т.е. итератор все еще указывает на старую позицию, и мы точно не знаем, что с тех пор произошло.
std::cout << *it << '\n'; // плохо плохо плохо!
}
6. Компиляция и запуск программы приводят к идеальному выполнению. Приложение не дает сбой, но данные, которые оно выводит на экран при разыменовании некорректного указателя, выглядят совершенно случайными. В таком виде программу оставлять нельзя, но к этому моменту никто не сообщит нам о данной ошибке, если мы не заметим ее сами (рис. 3.5).
7. Ситуацию спасут флаги отладки! Реализация GNU STL поддерживает макрос препроцессора _GLIBCXX_DEBUG, который активизирует много функций для проверки достоверности STL. Это замедляет выполнение программы, но зато помогает находить ошибки. Можно активизировать макрос, добавив флаг -D_GLIBCXX_DEBUG в командную строку компилятора или определив его в начале файла кода, поместив его до директив
include
. Как видите, он завершает работу приложения, после чего запускаются разные средства очистки. Скомпилируем код с флагом для активизации проверяемых итераторов (в компиляторе Microsoft Visual C++ выглядит как /D_ITERATOR_DEBUG_LEVEL=1) (рис. 3.6).
8. Реализация STL для LLVM/clang тоже имеет флаги отладки, но они нужны для отладки самой STL, а не пользовательского кода. Для последнего можно активизировать различные средства очистки. Скомпилируем код для
clang
с помощью флагов -fsanitize=address
-fsanitize=undefined
и посмотрим, что произойдет (рис. 3.7).
Ого! Перед нами очень точное описание того, что именно пошло не так. Если бы мы не обрезали этот скриншот, то он занял бы несколько страниц книги. Обратите внимание: это характерно не только для
clang
, но и для GCC.
Если вы видите ошибки во время выполнения программы из-за того, что отсутствует какая-то библиотека, то значит, ваш компилятор не поставляется с библиотеками
libasan
и libubsan
. Попробуйте установить их с помощью вашего менеджера пакетов или чего-то аналогичного.
Как это работает
Как видите, нам ничего не нужно менять в программе, чтобы включить эту функциональность для кода, генерирующего ошибки. Мы, по сути, получили ее бесплатно, просто добавив некоторые флаги компилятора в командную строку при компиляции программы.
Эта возможность реализуется средствами очистки. Обычно это дополнительный модуль компилятора и библиотека, работающая во время выполнения программы. При активизации средства очистки компилятор добавит дополнительную информацию и код в бинарный файл, который представляет собой нашу программу. Во время выполнения библиотеки средства очистки, связанные с бинарным файлом программы, например, заменяют функции
malloc
и free
, чтобы проанализировать, как программа работает с получаемой памятью.Средства очистки помогают обнаруживать различные баги. Перечислю лишь несколько полезных примеров.
□Выход за пределы диапазона: ошибка случается, когда мы получаем доступ к элементу массива, вектора или аналогичного контейнера, лежащему за пределами корректной области памяти.
□Использование после освобождения: ошибка происходит, если мы обращаемся к памяти кучи после того, как она была освобождена (что мы и сделали в данном разделе).
□Переполнение переменной типа int
: ошибка появляется, если целочисленная переменная переполняется в результате подсчета значений, которые в нее не помещаются. Для целых чисел со знаком это приведет к неопределенному поведению.
□Выравнивание указателя: в некоторых архитектурах невозможно обратиться к памяти, если блоки памяти выровнены неаккуратно.
Средства очистки могут обнаруживать и многие другие ошибки.
Вы не всегда можете активизировать все доступные средства очистки, поскольку это замедляет программу. Однако хорошим тоном является их полная активизация в блочных и интеграционных тестах.
Дополнительная информация
Существует множество средств очистки для разных категорий ошибок, и все они еще разрабатываются. Мы можем и должны информировать других пользователей о том, как они могут улучшить свои тесты. На домашних страницах проектов GCC и LLVM в разделе документации перечислены доступные средства очистки:
□ https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html;
□ http://clang.llvm.org/docs/index.html (найдите в содержании раздел Sanitizers (Средства очистки)).
Каждый программист должен знать о тестировании с использованием средств очистки и всегда уметь его выполнять. К сожалению, в пугающе большом количестве компаний этого не происходит, несмотря даже на то, что код с ошибками — самая главная точка входа для вредоносного ПОи компьютерных вирусов.
Заняв должность разработчика в новой для вас компании, сразу убедитесь, что в вашей команде используются все доступные средства очистки. Если это не так, то у вас есть уникальный шанс исправить важные и незаметные ошибки в свой первый рабочий день!
Создаем собственный адаптер для итераторов-упаковщиков
Работа с разными языками программирования требует использования различных стилей программирования. Это неудивительно, поскольку каждый язык программирования был разработан для решения конкретных задач.
Существует особый стиль программирования — чистое функциональное программирование. Он значительно отличается от императивного, к которому привыкли программисты, работающие на С и С++. Несмотря на то, что этот стиль значительно отличается от других, во многих ситуациях он позволяет писать очень элегантный код.
Один из примеров проявления данной элегантности — реализация формул, например скалярного произведения. Если даны два математических вектора, то нахождение их скалярного произведения означает попарное умножение чисел на одинаковых позициях вектора, а затем суммирование этих умноженных значений. Скалярное произведение векторов
(a,b,c)*(d,e,f)
равно (a*e+b*e+c*f)
[6]. Конечно, это можно сделать и с помощью языков C и C++. Код выглядел бы следующим образом: