Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ООП_ Лекция №10 - Более сложные формы композиции.docx
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
68.53 Кб
Скачать

Использование 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: программист сосредотачивается на высокоуровневой логике задачи, полностью абстрагируясь от чисто технических рутинных вопросов управления памятью и борьбы с ее утечками.