- •Ооп: Лекция 10. Более сложные формы композиции.
- •Типичные ошибки при реализации родительских классов
- •Рекомендованный стиль интерфейса родительских классов
- •Классы-родители с применением итераторов контейнеров
- •Умные указатели std::unique_ptr
- •Использование std::unique_ptr для композиции объектов
- •Итерирование дочерних элементов с учетом умных указателей
- •Использование средства std::function
- •Использование отображений при реализации композиции
- •Использование множеств
- •Композиция с кратностью многие-ко-многим
- •Полные примеры из лекции
Умные указатели std::unique_ptr
Рассмотрим следующий набросок кода, иллюстрирующий часто повторяющуюся незаметную ошибку:
// Некий полезный класс с важным методом
class MyClass
{
public:
MyClass ( int _x );
void doSomethingImportant ();
};
void f ( int _x )
{
// Выделяем объект полезного класса в динамической памяти
MyClass * pObject = new MyClass( _x );
// Если такое условие не соблюдается, выходим из функции
if ( _x < 0 )
return;
// … сделать что-нибудь нужное с объектом pObject …
pObject->doSomethingImportant();
// Уничтожаем объект
delete pObject;
}
Предположим, функция f вызвана с отрицательным аргументом, т.е. условие, приводящее к немедленному выходу из функции выполняется. До блока проверки условия в динамической памяти был выделен объект. При нормальном сценарии такой объект используется, а затем уничтожается. Однако если условие сработает и управление досрочно выйдет из функции, произойдет не что иное, как банальная утечка памяти. К сожалению, это событие произойдет незаметно, без сирены или пугающих диалогов при запуске, но память останется недоступной до завершения программы.
Другой типичный сценарий для утечки памяти - это выброс исключения (инструкция throw) между моментами выделения ресурса и его освобождения.
Стандартная библиотека содержит прекрасное решение для предотвращения проблемы подобных утечек памяти - “умные указатели” (smart pointers). При этом, обычные указатели при сравнении с умными называют “сырыми” (raw pointers). Под умным указателем понимают специальные объекты, представляющих собой объектно-ориентированные обертки над обычными указателями, автоматизирующие рутинные задачи. В частности, основной задачей умных указателей является гарантированное освобождение выделенного ресурса.
Все стандартные умные указатели подключаются к программе через заголовочный файл <memory>. Имеется несколько разновидностей стандартных умных указателей:
std::unique_ptr - простейший умный указатель, “защелкивающий” вверенный ему сырой указатель, гарантированно уничтожающий его в деструкторе;
std::shared_ptr - более сложный умный указатель, основанный на подсчете ссылок на общий разделяемый сырой указатель, освобождающий ресурс, когда уничтожается последний обладатель - применяется, в первую очередь, в случаях, когда дочерний объект не имеет однозначно определяемого объекта-родителя (ответственность за уничтожение размыта);
std::weak_ptr - дополнение к std::shared_ptr, предназначенное для разрыва циклических связей между двумя объектами, ссылающимися друг на друга при помощи std::shared_ptr (применяется в узко специализированных ситуациях);
std::auto_ptr - ранний эквивалент std::unique_ptr до принятия стандарта С++’11, уничтожает вверенный объект в деструкторе, передает ответственность за уничтожение при копировании и присвоении (в стандарте С++’11 класс std::auto_ptr признан устаревшим и более не рекомендован, однако он часто встречается на практике в более старом коде).
Кроме того, дополнительные варианты умных указателей с более сложным поведением часто предлагаются сторонними библиотеками. В контексте рассматриваемой дисциплины и, в частности, темы композиции объектов, наибольший интерес представляют умные указатели std::unique_ptr. С работой остальных классов данной категории рекомендуется ознакомиться самостоятельно.
Итак, перепишем приведенный выше пример с использованием std::unique_ptr:
#include <memory>
void f ( int _x )
{
// Выделяем объект полезного класса в динамической памяти.
// Незамедлительно помещаем его в умный указатель.
std::unique_ptr< MyClass > pObject( new MyClass( _x ) );
// Если такое условие не соблюдается, выходим из функции
if ( _x < 0 )
return;
// … сделать что-нибудь нужное с объектом pObject …
pObject->doSomethingImportant();
}
По сравнению с предыдущим вариантом здесь внесено 2 важных изменения:
для динамически создаваемого объекта создается обертка std::unique_ptr, и сырой указатель, возвращенный вызовом new, моментально помещается внутрь обертки;
в конце функции больше нет вызова оператора delete.
Ключевой аспект поведения умных указателей, требующий понимания - это автоматическое удаление помещенного в них динамически выделенного объекта в деструкторе. Сам умный указатель создается как обычный объект на стеке, т.е. его деструктор вызывается при выходе из текущей области видимости. Если функция выполняется полностью, его деструктор будет неявно вызван в конце по выходу из всех инструкций. Если же функция прерывается пользовательским условием, его деструктор будет все равно вызван до того, как управление будет передано функции выше по стеку. В любом случае, вверенный в управление объект будет гарантированно уничтожен.
Еще один интересный момент связан с применением оператора доступа к членам класса через указатель ->. С точки зрения синтаксиса такой вызов выглядит абсолютно аналогично работе с обычным сырым указателем, однако за такой записью стоит перегруженный в std::unique_ptr оператор ->. Такой оператор, как правило, просто возвращает хранимый сырой указатель, а компилятор обеспечивает возможность вызова методов или обращения к полям.
Помимо автоматического удаления и доступа к членам сырого указателя, следует отметить такие особенности работы с std::unique_ptr:
объекты класса std::unique_ptr нельзя копировать ни при конструировании, ни при присвоении - это ограничение является весьма логичным, поскольку сразу две обертки не должны отвечать за уничтожение одного и того же вверенного сырого указателя;
объекты класса std::unique_ptr можно и нужно перемещать, что позволяет возвращать их как результат функций вместо сырых указателей
std::unique_ptr< MyClass > someFunc ( int _x )
{
return std::unique_ptr< MyClass >( new MyClass( _x ) );
}
запись по созданию умного указателя одновременно с сырым указателем по его аргументам можно значительно упростить, применяя вспомогательную функцию std::make_unique:
std::unique_ptr< MyClass > someFunc ( int _x )
{
// Эквивалентно полному варианту записи: // return std::unique_ptr< MyClass >( new MyClass( _x ) )
return std::make_unique< MyClass >( _x ) );
}
для получения ссылки на вверенный объект перегружен оператора разыменования *:
std::unique_ptr< MyClass > pObject( new MyClass( _x ) );
MyClass const & ref = * pObject;
как и сырые указатели, умные указатели можно использовать в условиях, т.к. у них перегружен оператор явного преобразования к типу bool:
if ( pObject )
// do something
чтобы отцепить вверенный объект от умного указателя без уничтожения (допустим, какая-то другая часть программы берет на себя ответственность за его уничтожение) можно воспользоваться методом release():
std::unique_ptr< MyClass > pObject( new MyClass() );
MyClass * pRawPtr = pObject.release();
// Результат: ответственность за уничтожение снимается
вверенный объект может быть заменен на другой либо nullptr при помощи метода reset(), при этом прежний вверенный объект будет уничтожен:
std::unique_ptr< MyClass > pObject( new MyClass() );
// результат: первый объект уничтожен, второй прикреплен
pObject->reset( new MyClass() );
// результат: второй объект уничтожен, никто не прикреплен
pObject->reset();
Фактически, работа с умными указателями не слишком отличается от работы с сырыми указателями, но при этом, программист надежно защищен от случайных утечек памяти. Использование умных указателей является центральной в популярной в литературе идиоме программирования RAII (Resource Acquisition Is Initialization - выделение ресурса есть инициализация). Эта идиома касается не только языка С++ и не только динамической памяти, а любых видов ресурсов (файлов, сетевых соединений, мьютексов и других ресурсов). Ключевая мысль состоит в том, что не имеет практического смысла пытаться продумывать и тестировать не забыл ли программист освободить ресурс в том или ином сценарии, вместо этого необходим механизм гарантированного освобождения, где программисту негде было бы ошибиться.
