- •Ооп: Лекция 10. Более сложные формы композиции.
- •Типичные ошибки при реализации родительских классов
- •Рекомендованный стиль интерфейса родительских классов
- •Классы-родители с применением итераторов контейнеров
- •Умные указатели std::unique_ptr
- •Использование std::unique_ptr для композиции объектов
- •Итерирование дочерних элементов с учетом умных указателей
- •Использование средства std::function
- •Использование отображений при реализации композиции
- •Использование множеств
- •Композиция с кратностью многие-ко-многим
- •Полные примеры из лекции
Использование std::unique_ptr для композиции объектов
Умный указатель std::unique_ptr можно использовать для значительного упрощения реализации отношения композиции с ответственностью за уничтожение. Такой интеллектуальной обертки вполне достаточно, чтобы сосредоточиться на реализации полезной функциональности класса, а уничтожение объекта будет происходить гарантированно и автоматически.
Простейшим полезным случаем для применения умного указателя std::unique_ptr является одиночная композиция с ответственностью за уничтожение. Ниже приведен пример такой композиции между классом-родителем Student и дочерним классом Scolarship (стипендия). Стипендия может быть определена или не определена, такая связь не является обязательной. В случае если она определяется, объект Student должен отвечать за ее уничтожение. Отношение между этими объектами может быть разорвано, если студент нарушит правила начисления стипендии (например, завалит сдачу дисциплины ООП).
В коде примера данное отношение между студентом и стипендией реализуется с применением std::unique_ptr, демонстрируются основные приемы его использования для одиночных объектов:
scolarship.hpp
#ifndef _SCOLARSHIP_HPP_
#define _SCOLARSHIP_HPP_
//************************************************************************
#include <stdexcept>
//************************************************************************
// Класс, моделирующий стипендию
class Scolarship
{
/*-----------------------------------------------------------------*/
public:
/*-----------------------------------------------------------------*/
// Конструктор, принимает объем стипендии и признак персональности
Scolarship ( double _amount, bool _personal )
: m_amount( _amount )
, m_personal( _personal )
{
// Инвариант: объем стипендии должен быть положительным числом
if ( m_amount <= 0.0 )
throw std::logic_error( "Non-positive amount" );
}
// Метод доступа к объему
double getAmount () const { return m_amount; }
// Метод доступа к персональности
bool isPersonal () const { return m_personal; }
/*-----------------------------------------------------------------*/
private:
/*-----------------------------------------------------------------*/
// Объем стипендии
double m_amount;
// Персональность
bool m_personal;
/*-----------------------------------------------------------------*/
};
//************************************************************************
#endif // _SCOLARSHIP_HPP_
student.hpp
#ifndef _STUDENT_HPP_
#define _STUDENT_HPP_
//************************************************************************
#include "scolarship.hpp"
#include <memory>
//************************************************************************
// Класс, моделирующий студента
class Student
{
/*-----------------------------------------------------------------*/
public:
/*-----------------------------------------------------------------*/
// Конструктор, принимает имя студента
Student ( std::string const & _name )
: m_name( _name )
{}
// Метод доступа к имени студента
std::string const & getName () const { return m_name; }
// Метод, выясняющий есть ли у студента стипендия
bool hasScolarship () const
{
// Выясняем у умного указателя вверен ли ему какой-либо объект:
return m_scolarship.get() != nullptr;
}
// Метод, возвращающий описатель назнченной студенту стипендии
Scolarship const & getScolarship () const
{
// Вернуть описатель стипендии, если она была назначена
if ( hasScolarship() )
return * m_scolarship; // Разыменование через средства умного указателя
else
// Ошибка: у данного студента нет стипендии
throw std::logic_error( "Student has no scolarship" );
}
// Метод, назначающий студенту стипендию
void assignScolarship ( std::unique_ptr< Scolarship > _scolarship )
{
// Замещение дочернего объекта, перемещение содержимого из умного указателя
m_scolarship = std::move( _scolarship );
}
// Метод, лишающий студента стипендии
void scolarshipRulesViolated ()
{
// Сброс умного указателя, уничтожение вверенного объекта
m_scolarship.reset();
}
/*-----------------------------------------------------------------*/
private:
/*-----------------------------------------------------------------*/
// Имя студента
std::string m_name;
// Описатель стипендии, обернутый в умный указатель
std::unique_ptr< Scolarship > m_scolarship;
/*-----------------------------------------------------------------*/
};
//************************************************************************
#endif // _STUDENT_HPP_
Итак, в приведенном примере связь между объектом Student и Scolarship реализована с применением умного указателя std::unique_ptr. Внешне, такое решение кажется даже немного более сложным, чем аналогичное на основе обычных указателей - требуется вызов методов, использование перемещения объектов при установлении связи и т.д. В чем же преимущество?
Внимательный пересмотр решения дает очевидный вывод - в коде отсутствует явно заданный деструктор, удаляющий дочерний объект. С применением умных указателей, решение упрощается, поскольку автоматически сгенерированный компилятором деструктор вызовет уничтожение на объекте std::unique_ptr без вмешательства программиста. В свою очередь, умный указатель гарантирует, что удалится дочерний объект. Также, отпадает необходимость в написании явного запрета для конструктора копий и оператора копирующего присвоения: автоматическая версия не может быть успешно сгенерирована, поскольку копирование и так запрещено в классе std::unique_ptr.
Умные указатели также удобно применять для реализации множественной композиции с ответственностью за уничтожение, что и представлено в обновленном уже рассмотренном примере о книгах и главах. Решение выглядит следующим образом:
book.hpp
#ifndef _BOOK_HPP_
#define _BOOK_HPP_
/*****************************************************************************/
#include <string>
#include <vector>
#include <memory>
/*****************************************************************************/
class Chapter;
/*****************************************************************************/
class Book
{
/*-----------------------------------------------------------------*/
public:
/*-----------------------------------------------------------------*/
// Конструктор
Book ( std::string const & _title );
// Конструктор со списком глав
Book (
std::string const & _title,
std::initializer_list< Chapter * > _chapters
);
// Метод доступа к названию главы
std::string const & getTitle () const;
// Метод, возвращающий количество глав
int getChaptersCount () const;
// Метод, подтверждающий наличие главы в книге
bool hasChapter ( Chapter const & _chapter ) const;
// Метод, добавляющий главу в конец книги
void addChapter ( std::unique_ptr< Chapter > _chapter );
// Метод, удаляющий указанную главу из книги
void removeChapter ( Chapter const & _chapter );
// Метод удаления всех глав
void clearChapters ();
/*-----------------------------------------------------------------*/
// работа с итераторами ...
/*-----------------------------------------------------------------*/
private:
/*-----------------------------------------------------------------*/
// Набор глав, хранящихся в виде умных указателей
std::vector< std::unique_ptr< Chapter > > m_chapters;
// Название книги
const std::string m_title;
/*-----------------------------------------------------------------*/
};
/*****************************************************************************/
// Реализация метода доступа к названию главы
inline std::string const &
Book::getTitle () const
{
return m_title;
}
/*****************************************************************************/
// Реализация метода доступа к количеству глав
inline int
Book::getChaptersCount () const
{
return m_chapters.size();
}
/*****************************************************************************/
// Методы работы с итераторами ...
/*****************************************************************************/
#endif // _BOOK_HPP_
book.cpp
/*****************************************************************************/
#include "book.hpp"
#include "chapter.hpp"
/*****************************************************************************/
// Реализация конструктора
Book::Book ( std::string const & _title )
: m_title( _title )
{}
/*****************************************************************************/
// Реализация конструктора со списком глав
Book::Book (
std::string const & _title
, std::initializer_list< Chapter * > _chapters
)
: m_title( _title )
{
// Обходим список инициализаторов и поэлементно добавляем главы в книгу
for ( Chapter * pChapter : _chapters )
// Для добавления главы сразу оборачиваем сырые указатели в умные обертки
addChapter( std::unique_ptr< Chapter >( pChapter ) );
}
/*****************************************************************************/
// Реализация метода, подтверждающего наличие главы в книге
bool Book::hasChapter ( Chapter const & _chapter ) const
{
int nChapters = getChaptersCount();
for ( int i = 0; i < nChapters; i++ )
// Извлекаем сырой указатель из умного при помощи метода get(),
// сравниваем его с образцом
if ( m_chapters[ i ].get() == & _chapter )
return true;
return false;
}
/*****************************************************************************/
// Реализация метода добавления главы в конец книги
void Book::addChapter ( std::unique_ptr< Chapter > _chapter )
{
// Объекты std::unique_ptr не копируются, но зато эффективно перемещаются
m_chapters.push_back( std::move( _chapter ) );
}
/*****************************************************************************/
// Реализация метода удаления указанной главы
void Book::removeChapter ( Chapter const & _chapter )
{
// Находим нужную главу
int nChapters = getChaptersCount();
for ( int i = 0; i < nChapters; i++ )
if ( m_chapters[ i ].get() == &_chapter )
{
// Достаточно убрать элемент из нужной позиции вектора
// Уничтожение этой главы происходит автоматически!
m_chapters.erase( m_chapters.begin() + i );
return;
}
// Ошибка: такой главы нет
throw std::logic_error( "Chapter does not exists in book" );
}
/*****************************************************************************/
// Реализация метода очистки всех глав книги
void Book::clearChapters ()
{
// Достаточно просто очистить сам вектор.
// Элементы уничтожатся автоматически!
m_chapters.clear();
}
/*****************************************************************************/
test.cpp
#include <iostream>
#include <algorithm>
#include "book.hpp"
#include "chapter.hpp"
/*****************************************************************************/
std::unique_ptr< Book > createTestBook ()
{
std::unique_ptr< Book > pBook( new Book( "Some Title" ) );
pBook->addChapter( std::make_unique< Chapter >( "AAA", 12 ) );
pBook->addChapter( std::make_unique< Chapter >( "BBB", 15 ) );
pBook->addChapter( std::make_unique< Chapter >( "CCC", 10 ) );
return pBook;
}
/*****************************************************************************/
int main ()
{
std::unique_ptr< Book > pBook = createTestBook();
for ( auto const & pChapter : pBook->chapters() )
std::cout << pChapter->getTitle() << std::endl;
}
/*****************************************************************************/
Изменения по сравнению с вариантом из предыдущей лекции заключаются в следующем:
вместо хранения данных в виде вектора сырых указателей, теперь используется вектор умных указателей std::unique_ptr;
это решение моментально позволяет упростить код за счет:
удаления деструктора - теперь все дочерние объекты уничтожаются автоматически;
избавления от явного объявления удаленных конструктора копий и оператора присвоения - поскольку объект становится некопируемым без вмешательства программиста (std::unique_ptr сам запрещает копирование);
значительного упрощения функций clearChapters и removeChapter - забота здесь идет только об очистке вектора от удаляемых элементов, а само содержимое элементов уничтожается самостоятельно полностью автоматически;
новая глава приходит в метод addChapter в виде умного указателя, содержимое которого перемещается в контейнер глав.
Теперь становится окончательно виден позитивный эффект от применения std::unique_ptr: программист сосредотачивается на высокоуровневой логике задачи, полностью абстрагируясь от чисто технических рутинных вопросов управления памятью и борьбы с ее утечками.
