Классы-родители с применением итераторов контейнеров 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Классы-родители с применением итераторов контейнеров



 

Предложенное решение, изолирующее конкретный использованный контейнер за типовыми рекомендованными методами, вполне пригодно на практике. Однако и его можно улучшить, данная форма реализации далеко не является пределом совершенства.

 

В реализации имеется ряд методов, предполагающих работу с порядковыми номерами дочерних объектов, а именно:

 

Chapter & getChapter (int _index) const;

int findChapterIndex (Chapter const & _chapter) const;

void insertChapter (int _atIndex, Chapter * _pChapter);

void removeChapter (int _atIndex);

 

Для глав в книге нумерация по порядку вполне естественна и может быть использована не только для получения доступа к конкретному элементу-главе, но и как полезная функциональность сама по себе. Однако на практике часто встречаются случаи, в которых роль порядковых номеров значительно ниже. Если порядок дочерних объектов роли не играет, использование порядковых номеров лишь для перебора содержимого набора кажется избыточным.

 

Кроме того, работа с порядковыми номерами несколько сковывает программиста в выборе конкретной структуры данных. Хотя зависимости хорошо расщеплены, заменить вектор на связный список или множество в условиях, когда интерфейс требует вернуть i-ый объект по порядку, может быть проблематичным. Безусловно, реализовать такое требование на основе других структур технически возможно, но решение не будет слишком эффективно. Перебор вместо линейного алгоритма превратится в квадратичный (каждый последующий элемент будет извлекаться перебором с начала). Просто и эффективно будет лишь заменить вектор на массив либо любую другую структуру, где поддерживается эффективный произвольный доступ к элементам по индексу и оператор или метод для индексной выборки (например, контейнер std::deque).

 

Чтобы более явно выразить намерение программиста, можно видоизменить набор стандартных операций для работы с дочерними объектами, выбросить лишние операции с порядковыми номерами, и, в итоге, упростить интерфейс класса-родителя до следующей формы:

 

class Book

{

/*-----------------------------------------------------------------*/

 

public:

 

/*-----------------------------------------------------------------*/

 

//...

 

// Метод, возвращающий количество глав

int getChaptersCount () const;

 

// Метод, добавляющий главу

void addChapter (Chapter * _pChapter);

 

// Метод, подтверждающий наличие главы в книге

bool hasChapter (Chapter const & _chapter) const;

 

// Метод удаления указанной главы

void removeChapter (Chapter const & _chapter) const;

 

// Метод удаления всех глав

void clearChapters ();

 

/*-----------------------------------------------------------------*/

 

};

 

Теперь место вектора может свободно занять абсолютно любой стандартный контейнер и пользовательский контейнер без потери эффективности. Требуемые операции достаточно абстрактны и подходят практически к любой структуре данных. В том числе, в реализации могут быть применены и более сложные варианты, например, множества или отображения, не применяющие алгоритм линейного поиска, например, основанные на хэш-таблицах или бинарных деревьях поиска..

 

Однако, в ходе такого преобразования незаметно теряется неявная возможность перебора элементов. В прежних версиях эту роль играла пара методов getXXXCount() и getXXX(int), позволявшие организовать простой цикл for для прохода по контейнеру и перебора всех элементов. В качестве замены этому примитивному механизму перебора следует использовать итераторы. Как уже было показано ранее, итераторы хорошо расщепляют зависимости между реализацией хранения данных и кодом их обработки. Любой контейнер стандартной библиотеки предоставляет итераторы для полного прохода от начала и до конца при помощи пары методов begin и end. Если вернуть итераторы клиентскому коду, будет возможен обход набора дочерних объектов, не использующий порядковые номера, и одновременно не нарушающий инкапсуляцию.

 

Объявление typedef позволяет скрыть от клиенского кода сложности объявления конкретного типа итератора, относящегося к используемому контейнеру. Если контейнер изменится на другой, достаточно будет обновить объявление typedef, и большинство клиентского кода не будет задето.

 

class Book

{

/*-----------------------------------------------------------------*/

 

public:

 

/*-----------------------------------------------------------------*/

 

// …

 

// Синоним для типа итератора, перебирающего вектор глав

typedef std::vector< Chapter * >::const_iterator ChapterIterator;

 

// Методы, возвращающие начальный и конечный итераторы контейнера глав

ChapterIterator chaptersBegin() const { return m_chapters.begin(); }

ChapterIterator chaptersEnd() const { return m_chapters.end(); }

 

/*-----------------------------------------------------------------*/

 

};

 

Ниже приведен фрагмент кода с перебором глав книги на основе итераторов:

 

//...

 

int main ()

{

// Создаем тестовую книги двумя способами

Book * pBook = createTestBook();

 

// Перебираем главы первой книги через итераторы

Book::ChapterIterator itChapter = pBook->chaptersBegin();

while (itChapter!= pBook->chaptersEnd())

{

const Chapter & chapter = ** itChapter;

std::cout << chapter.getTitle() << std::endl;

++ itChapter;

}

 

// Уничтожаем книгу

delete pBook;

}

 

Выражение, извлекающее итератор можно записать чуть более компактно:

 

auto itChapter = pBook->chaptersBegin();

 

При таком подходе, следует возвращать клиентскому коду только такие итераторы, которые позволяют лишь перебирать и извлекать элементы (использование вложенного синонима типа const_iterator). Не рекомендуется предоставлять более сложные типы итераторов (вложенный синоним типа iterator), дающие права на модификацию, удаление, переупорядочивание элементов в контейнере. Допускать возврат неконстантных итераторов можно только в тех редких случаях, когда внешнее изменение не может привести к нарушению инвариантов родительского класса. Такая ситуация встречается редко. Практически невозможно встретить такой случай, если родитель отвечает за уничтожение дочерних объектов.

 

Помимо простого цикла while, использующего итераторы, может быть полезным применение нового интервального цикла for. Напомним, такой цикл требует, чтобы итерируемый объект-контейнер имел методы под названием begin/end, которые возвращают итераторы. В классе Book есть похожие методы, однако они носят другие названия. Переименовывать их в требуемые begin и end может быть не совсем логичным, особенно если родительский объект содержит более одного набора дочерних объектов (например, набор авторов книги помимо набора глав). Чтобы добиться работы интервальных циклов без собственных методов begin/end в родительском классе, необходимо небольшое расширение кода - возвращать из книги вспомогательную структуру (IterableChapters), обладающую нужными операциями begin и end, но при этом всю работу должны выполнять стандартные итераторы контейнера глав:

 

class Book

{

/*-----------------------------------------------------------------*/

 

public:

 

/*-----------------------------------------------------------------*/

 

// …

 

// Вспомогательный класс для реализации интервального цикла for по главам книги

class IterableChapters

{

public:

 

// Конструктор - запоминает переданную пару итераторов на контейнер глав

IterableChapters (ChapterIterator _chaptersBegin,
ChapterIterator _chaptersEnd)

: m_begin(_chaptersBegin), m_end(_chaptersEnd)

{}

 

// Методы begin/end используются интервальным циклом for

ChapterIterator begin () const { return m_begin; }

ChapterIterator end () const { return m_end; }

 

private:

 

// Итераторы на начало и конец контейнера глав соответственно

ChapterIterator m_begin, m_end;

};

 

 

// Методы, возвращающий итерируемый объект для перебора глав книги

IterableChapters chapters () const

{

return IterableChapters(chaptersBegin(), chaptersEnd());

}

 

/*-----------------------------------------------------------------*/

 

};

 

Теперь для перебора и обработки глав можно применить более удобный интервальный цикл for:

 

int main ()

{

// Создаем тестовую книги двумя способами

Book * pBook = createTestBook();

 

// Перебираем главы первой книги через интервальный цикл for

for (Chapter * pChapter: pBook->chapters())

std::cout << pChapter->getTitle() << std::endl;

 

// Уничтожаем книгу

delete pBook;

}

 

Такая запись очень компакта и удобна. Высокий уровень абстракции способствует лучшему восприятию сути решения задачи при чтении кода. Однако не стоит забывать что стоит за такой выразительностью. Кажущаяся простота на самом деле скрывает сразу несколько слоев обработки:

● интервальный цикл for скрывает за собой эквивалентный цикл while, использующий итераторы объекта из правой части, полученные через вызовы begin()/end();

● вызов pBook->chapters() возвращает вспомогательную структуру IterableChapters, которая скрывает связь с итераторами контейнера глав, выполняющими перебор элементов;

● итераторы глав - это итераторы конкретной реализации класса std::vector из стандартной библиотеки, которые скрывают проход через конкретную реализацию структуры данных;

● фактически, любой вектор содержит массив элементов, а итератор прямо или косвенно задействует указатели на элементы в этом массиве.

 

Каждый слой не является чем-то сложным сам по себе, однако их композиция образует целую систему. Этот пример наглядно демонстрирует потенциал методов абстракции и декомпозиции, лежащих в основе методов объектно-ориентированного программирования.

 

В то же время, такая сложная многоуровневая структура абстракций не оказывает негативного влияния на производительность генерируемого компилятором машинного кода. В отличие от других объектно-ориентированных языков, С++ предоставляет возможности встраивания функций в место вызова, при чем встраивание может быть многоуровневым. Хотя интервальный цикл for в функции main и задействует 4 уровня абстракций, благодаря встраиванию машинный код будет фактически напрямую будет работать с массивом и указателями на его элементы. В результате, программа одновременно является и высокоуровневой, и быстродействующей.

 

Ссылку на полный вариант реализации композиции классов глава-книга с применением итераторов можно найти в конце лекции.

 

Умные указатели 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 - выделение ресурса есть инициализация). Эта идиома касается не только языка С++ и не только динамической памяти, а любых видов ресурсов (файлов, сетевых соединений, мьютексов и других ресурсов). Ключевая мысль состоит в том, что не имеет практического смысла пытаться продумывать и тестировать не забыл ли программист освободить ресурс в том или ином сценарии, вместо этого необходим механизм гарантированного освобождения, где программисту негде было бы ошибиться.

 

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

 



Поделиться:


Последнее изменение этой страницы: 2017-02-17; просмотров: 82; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 18.119.159.150 (0.407 с.)