Типичные ошибки при реализации родительских классов 


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



ЗНАЕТЕ ЛИ ВЫ?

Типичные ошибки при реализации родительских классов



Inline int

Book::getChaptersCount () const

{

return m_chapters.size();

}

 

/*****************************************************************************/

 

// Реализация метода доступа к главе по порядковому номеру

inline Chapter &

Book::getChapter (int _index) const

{

return * m_chapters.at(_index);

}

 

/*****************************************************************************/

 

#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), m_chapters(_chapters)

{}

 

/*****************************************************************************/

 

// Реализация деструктора

Book::~Book ()

{

// Очистка уничтожит каждую главу

clearChapters();

}

 

/*****************************************************************************/

 

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

int Book::findChapterIndex (Chapter const & _chapter) const

{

int nChapters = getChaptersCount();

for (int i = 0; i < nChapters; i++)

if (m_chapters[ i ] == & _chapter)

return i; // глава найдена

 

return -1; // глава не найдена

}

 

/*****************************************************************************/

 

// Реализация метода, подтверждающего наличие главы в книге

bool Book::hasChapter (Chapter const & _chapter) const

{

// Если глава есть в книге, порядковый номер будет найден успешно

return findChapterIndex(_chapter)!= -1;

}

 

/*****************************************************************************/

 

// Реализация метода добавления главы в конец книги

void Book::addChapter (Chapter * _pChapter)

{

m_chapters.push_back(_pChapter);

}

 

/*****************************************************************************/

 

// Реализация метода добавления главы в указанной позиции

void Book::insertChapter (int _atIndex, Chapter * _pChapter)

{

m_chapters.insert(m_chapters.begin() + _atIndex, _pChapter);

}

 

/*****************************************************************************/

 

// Реализация метода удаления главы по порядковому номеру

void Book::removeChapter (int _atIndex)

{

delete m_chapters.at(_atIndex);

m_chapters.erase(m_chapters.begin() + _atIndex);

}

 

/*****************************************************************************/

 

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

void Book::removeChapter (Chapter const & _chapter)

{

int index = findChapterIndex(_chapter);

if (index == -1)

throw std::logic_error("Chapter does not exists in book");

 

removeChapter(index);

}

 

/*****************************************************************************/

 

// Реализация метода очистки всех глав книги

void Book::clearChapters ()

{

// Уничтожаем каждую главу

for (Chapter * pChapter: m_chapters)

delete pChapter;

 

// Очищаем контейнер

m_chapters.clear();

}

 

/*****************************************************************************/

 

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

 

Использование множеств

 

Множества применяются для реализации композиции объектов в тех случаях, когда набор дочерних объектов должен содержать только уникальные элементы. В принципе, такое ограничение можно поддержать и на обычном контейнере, таком как std::vector, но, также как и в задачах поиска, подтверждение уникальности будет основано на линейном переборе.

 

У книги может быть некоторый набор авторов. Разумеется, авторы не должны повторяться в списке в рамках одной книги, что создает предпосылку для использования множеств. Пусть имеется отдельный класс Author, описывающий автора книги:

 

author.hpp

 

#ifndef _AUTHOR_HPP_

#define _AUTHOR_HPP_

 

/*****************************************************************************/

 

#include <string>

 

/*****************************************************************************/

 

class Author

{

 

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

 

public:

 

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

 

// Конструктор: принимает имя автора и год его рождения

Author (std::string const & _name, int _birthYear);

: m_name(_name), m_birthYear(_birthYear)

{}

 

// Метод доступа к имени

std::string const & getName () const { return m_name; }

 

// Метод доступа к году рождения

int getBirthYear () const { return m_birthYear; }

 

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

 

private:

 

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

 

// Имя автора

const std::string m_name;

 

// Год рождения автора

const int m_birthYear;

 

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

 

};

 

/*****************************************************************************/

 

#endif // _AUTHOR_HPP_

 

 

Реализация книги дополняется множеством авторов (хэш-таблица указателей на объекты Author), а также типовым набором операций для формирования и просмотра множества. При этом, в отличие от глав, книга не отвечает за уничтожение авторов, поскольку они могут быть авторами и других книг. Ниже показаны расширения класса Book для обработки набора авторов:

 

book.hpp

 

/*****************************************************************************/

 

//...

 

#include <unordered_set>

 

class Author;

 

/*****************************************************************************/

 

class Book

{

 

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

 

public:

 

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

 

//...

 

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

int getAuthorsCount () const;

 

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

bool hasAuthor (Author const & _author) const;

 

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

void addAuthor (Author const & _author);

 

// Метод, отменяющий регистрацию одного из авторов книги

void removeAuthor (Author const & _author);

 

// Метод очистки набора авторов

void clearAuthors ();

 

// Метод обхода набора авторов с применением указанного пользовательского действия

void forEachAuthor (std::function< void (Author const &) > _action) const;

 

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

 

private:

 

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

 

//...

 

// Множество авторов книги

std::unordered_set< Author const * > m_authors;

 

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

 

};

 

/*****************************************************************************/

 

//...

 

// Реализация метода, возвращающего количество авторов

Inline int

Book::getAuthorsCount () const

{

return m_authors.size();

}

 

//...

 

/*****************************************************************************/

 

 

book.cpp

 

//...

 

/*****************************************************************************/

 

// Реализация метода, выясняющего является ли указанная персона автором данной книги

bool Book::hasAuthor (Author const & _author) const

{

// Поиск на множестве: если автор есть в составе, результат отличен от end()

return m_authors.find(& _author)!= m_authors.end();

}

 

/*****************************************************************************/

 

// Реализация метода, добавляющего очередного автора к книге

void Book::addAuthor (Author const & _author)

{

// Авторы не должны повторяться

if (hasAuthor(_author))

throw std::logic_error("Duplicate author");

 

// Добавляем автора во множество авторов данной книги

m_authors.insert(& _author);

}

 

/*****************************************************************************/

 

// Реализация метода удаления автора из книги

void Book::removeAuthor (Author const & _author)

{

// Ищем позицию автора во множестве

auto it = m_authors.find(& _author);

if (it == m_authors.end())

// Ошибка: такой автор не был зарегистрирован

throw std::logic_error("No such author");

 

// Удаляем автора из множества

m_authors.erase(it);

}

 

/*****************************************************************************/

 

// Реализация метода очистки набора авторов

void Book::clearAuthors ()

{

m_authors.clear();

}

 

/*****************************************************************************/

 

// Реализация метода обхода всех авторов с вызовом указанного пользовательскоо действия

void Book::forEachAuthor (std::function< void (Author const &) > _action) const

{

for (Author const * pAuthor: m_authors)

_action(* pAuthor);

// ^

// Клиентский код получит ссылку на автора

}

 

/*****************************************************************************/

 

Выводы

 

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

 

Позднее было введено понятие умных указателей, в частности, std::unique_ptr, и рассматривались способы их применения для упрощения реализации одиночной и множественной композиции с ответственностью за уничтожение дочерних объектов.

 

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

Типичные ошибки при реализации родительских классов

 

Использование реализации векторов и других структур данных из стандартной библиотеки является массовым каждодневным явлением в работе С++ программиста. Стандартная реализация std::vector берет на себя задачи эффективного управления памятью, может взаимодействовать с любым копируемым или перемещаемым типом данных, указываемым при помощи угловых скобок <>, предоставляет набор удобных методов для просмотра и модификации хранимых элементов. Программисту, решающему конкретную задачу, нужно лишь применить эту готовую реализацию в своем родительском классе в соответствии с приведенными выше образцами. Остаются только 2 вопросительных момента, требующих принятия программистом какого-либо решения, в частности:

● хранится ли набор дочерних объектов по значению или по указателю? (выбираем первое для классов-значений и второе для классов-сущностей);

● если дочерние объекты хранятся по указателю, отвечает ли родительский объект за их уничтожение, когда приходит пора уничтожать его самого? (зависит от требований задачи).

 

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

 

Рассмотрим типичные ошибки на примере композиции объектов, где имеется класс, моделирующий книгу (Book), содержащую набор глав (Chapter). Оба класса, очевидно, являются сущностями, отличаемыми по названию, при этом книга хранит главы с ответственностью за уничтожение.

 

Класс Chapter является простейшей абстракцией главы, содержит лишь данные о ее названии и количестве страниц, а также конструктор и методы доступа - здесь действительно негде ошибиться:

 

chapter.hpp

 

#ifndef _CHAPTER_HPP_

#define _CHAPTER_HPP_

 

/*****************************************************************************/

 

#include <string>

 

/*****************************************************************************/

 

class Chapter

{

 

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

 

public:

 

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

 

// Конструктор

Chapter (std::string const & _title, int _nPages)

: m_title(_title), m_nPages(_nPages)

{}

 

// Метод доступа к названию главы

std::string const & getTitle () const { return m_title; }

 

// Метод доступа к количеству страниц

int getPagesCount () const { return m_nPages; }

 

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

 

private:

 

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

 

// Название главы

const std::string m_title;

 

// Количество страниц

const int m_nPages;

 

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

 

};

 

#endif // _CHAPTER_HPP_

 

 

Класс Book немного сложнее. Объект-книга содержит название и набор глав в виде вектора указателей на объекты Chapter. Стоит отметить, что приведенный ниже код ужасен по концентрации нерациональности, и в таком духе ПИСАТЬ НИ В КОЕМ СЛУЧАЕ НЕ СЛЕДУЕТ! Но, к сожалению, подобные “трюки” весьма популярны.

 

Проблемные участки в коде реализации класса-книги выделены красным цветом:

 

book_bad.hpp

 

#ifndef _BOOK_BAD_HPP_

#define _BOOK_BAD_HPP_

 

/*****************************************************************************/

 

#include "chapter.hpp"

#include <vector>

 

/*****************************************************************************/

 

class Book

{

 

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

 

public:

 

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

 

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

Book (std::string _title, std::vector< Chapter * > _chapters)

: m_title(_title), m_chapters(_chapters)

{}

 

// Запрещенные конструктор копий и оператор присвоения

Book (const Book &) = delete;

Book & operator = (const Book &) = delete;

 

// Деструктор - уничтожает дочерние объекты-главы

~ Book ()

{

for (Chapter * pChapter: m_chapters)

delete pChapter;

}

 

// Метод доступа к названию

std::string getTitle () const { return m_title; }

 

// Метод доступа к вектору глав

std::vector< Chapter * > getChapters () const { return m_chapters; }

 

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

 

private:

 

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

 

// Набор глав в виде вектора указателей на объекты Chapter

std::vector< Chapter * > m_chapters;

 

// Название книги

std::string m_title;

 

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

 

};

 

/*****************************************************************************/

 

#endif // _BOOK_BAD_HPP_

 

Соответственно, использовать такой класс предполагается примерно следующим образом:

 

test_books_bad.cpp

 

#include "book_bad.hpp"

 

#include <iostream>

 

// Функция, создающая тестовую книгу с 3-мя главами

Book * createTestBook ()

{

// Делаем временный вектор и наполняем его объектами-главами

std::vector< Chapter * > chapters;

chapters.push_back(new Chapter("AAA", 12));

chapters.push_back(new Chapter("BBB", 15));

chapters.push_back(new Chapter(“CCC”, 10));

 

// Создаем объект-книгу, передаем название и временный вектор

return new Book("Some Title", chapters);

}

 

int main ()

{

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

Book * pBook = createTestBook();

 

// Извлекаем вектор глав из книги и печатаем на экране названия каждой из глав

std::vector< Chapter * > chapters = pBook->getChapters();

for (Chapter const * pChapter: chapters)

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

 

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

delete pBook;

}

 

Ну и что же “криминального” в этом, на первый взгляд, обычном, более менее чистом решении?

 

Проблема №1 лежит на поверхности, и заключается в расточительном копировании. Обратим внимание на список аргументов и список инициализации конструктора:

 

Book (std::string _title, std::vector< Chapter * > _chapters)

: m_title(_title), m_chapters(_chapters)

{}

 

И строка с названием книги (_title), и вектор указателей на главы (_chapters) передаются в конструктор книги по значению. В зависимости от способа вызова этого кноструктора, может произойти копирование. Тестовая программа использует конструктор следующим образом:

 

return new Book("Some Title", chapters);

 

Название передается в виде строкового литерала “Some Title”, однако конструктор предполагает передачу объекта std::string. По логике вещей, здесь должно происходить неявное создание временного объекта std::string из литерала, поскольку std::string содержит такой конструктор, не помеченный ключевым словом explicit. Затем должно происходить его копирование при передаче в функцию. Однако современные компиляторы генерируют более оптимальный код для такого случая, в частности, конструируют аргумент вызываемой функции сразу в нужном месте на стеке, без создания временного объекта и копирования. Кстати, такая полезная оптимизация не возможна в том случае, когда фактический аргумент не является временным объектом - его приходится копировать при передаче, оригинальный объект на вызывающей стороне не должен быть затронут:

 

std::string bookTitle = "Some Title";

return new Book(bookTitle, chapters);

// ^

// копирование объекта bookTitle при передаче в конструктор

 

Именно такая ситуация наблюдается со вторым аргументом chapters - с вектором указателей на главы. Фактический объект-вектор был создан и заполнен заранее, а теперь передается в конструктор книги по значению, что приводит к его копированию. Если хранение небольших строк (до 16 символов) хорошо оптимизировано в классе std::string и не приводит к выделению блока динамической памяти для хранения символов, то непустой вектор точно выделяет хотя бы небольшой блок памяти для хранения элементов, в том числе, с учетом возможного будущего роста. И такое выделение будет происходить для каждой копии вектора.

 

Помимо нерациональной техники при передаче аргументов, в списке инициализации для обоих полей активизируется конструктор копий. Оба класса - std::string и std::vector, очевидно, определяют собственные конструкторы копий, выделяют блоки динамической памяти, копируют данные - символы и указатели на главы соответственно.

 

: m_title(_title), m_chapters(_chapters)

 

Таким образом, такой конструктор и конкретный его вызов в тестовой программе приводят к:

● созданию 2 объектов std::string и 2-х кратному копированию символов (при конструировании аргумента и в списке инициализации);

● созданию 3 объектов std::vector и 3-х кратному копированию указателей:

○ вектор в функции createTestBook размещает указатели внутри себя;

○ второй вектор образуется при передаче аргумента по значению, копируется из первого;

○ третий вектор находится в объекте Book, копируется из второго в списке инициализаци.

 

Подобная расточительность наблюдается еще и в методах доступа. При возврате и строки, и векторы глав копируются вместе со всем содержимым и передаются вызывающему коду:

 

std::string getTitle () const { return m_title; }

 

std::vector< Chapter * > getChapters () const { return m_chapters; }

 

Как очевидно из приведенного анализа, столь поверхностный подход к программированию на С++ неприемлем. Язык и стандартная библиотека буквально “пропитаны” средствами для оптимизации решений, а такой код на этом фоне напоминает поедание черной икры столовыми ложками!

 

Как минимум, это решение следует улучшить передачей данных по ссылке, тем самым хотя бы убирая очевидно избыточные копирования:

 

Book (std::string const & _title, std::vector< Chapter * > const &_chapters)

: m_title(_title), m_chapters(_chapters)

{}

 

std::string const & getTitle () const { return m_title; }

 

std::vector< Chapter * > const & getChapters () const { return m_chapters; }

 

На вызов конструктора эти изменения никак не влияют, а при доступе к набору глав нужно также использовать ссылку, чтобы не создавать новый вектор:

 

std::vector< Chapter * > const & chapters = pBook->getChapters();

 

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

 

Если задуматься, явно создаваемый временный вектор при создании книги так же не имеет особого смысла, и можно передать набор указателей через список инициализаторов std::initializer_list:

 

Book * createTestBook ()

{

// Создаем главы

Chapter * pChAAA = new Chapter("AAA", 12);

Chapter * pChBBB = new Chapter("BBB", 15);

Chapter * pChCCC = new Chapter(“CCC”, 10);

 

// Создаем книгу

return new Book("Some Title", { pChAAA, pChBBB, pChCCC });

// ^........................^

// std::initializer_list< Chapter * >,

// из которого неявно формируется вектор

}

 

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

 

Book * createTestBook ()

{

// Создаем книгу с главами

return new Book(

"Some Title",

{ // это по-прежнему

new Chapter("AAA", 12), // std::initializer_list< Chapter * >,

new Chapter("BBB", 15), // по которому будет сформирован вектор

new Chapter(“CCC”, 10) // - аргумент конструктора Book

}

);

}

 

Проблема №2 носит более глубокий характер и состоит в недопонимании принципа инкапсуляции. Сколь бы ни был универсален и удачен стандартный класс std::vector, он представляет собой конкретную выбранную программистом для решения данной текущей задачи структуру данных, которая реализует абстрактное понятие набора (списка) объектов. В тот момент, когда std::vector начинает явно использоваться в списке аргументов открытого конструктора и как возвращаемое значение открытого метода доступа, создается нежелательная зависимость. Она заключается в том, что весь код, использующий класс Book, будет вынужден знать и использовать именно std::vector для передачи глав, как только понадобится создать книгу или перебрать ее главы.

 

Создание такого рода зависимостей от конкретного способа реализации набора данных осложняет последующую модификацию кода. Чем именно? Допустим, программист в результате тестирования и замеров производительности придет к выводу, что вместо вектора в данной конкретной задаче лучше использовать другую структуру данных, например, связные списки std::list или множества std::set. Чтобы внести такое изменение, потребуется не только исправить реализацию в классе Book, но и обновить весь код, который уже использует методы на основе вектора. Если таких место много, это трудоемкий процесс. А если речь идет о проектировании библиотеки, которой будут пользоваться многие сторонние программисты, процесс обновления будет не только трудоемким, но и очень болезненным - внешний код внезапно перестанет компилироваться после обновления библиотеки!

 

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

 

Чтобы достичь желаемого повышения уровня абстракции, имеет смысл поставить под сомнение необходимость передавать полный набор объектов-глав при создании книги. Скорее всего, более пригодный вариант состоит в создании пустой книги по одному лишь названию, а затем в постепенном добавлении в нее глав. Соответственно, в классе Book необходим конструктор, который принимает одно лишь название. Для добавления глав понадобится специальный метод addChapter. С технической точки зрения это изменение незначительно - метод addChapter всего лишь будет добавлять очередной объект-главу в вектор указателей при помощи метода push_back самостоятельно вместо использующего объект-книгу клиентского кода.

 

Если конструктор в прежней форме, когда все главы формируются заранее до создания книги, будет все же востребован, то следует передавать нечто не привязанное к конкретной структуре данных. Хороший кандидат на исполнение такого пожелания - объект std::initializer_list< Chapter * >, который позволит формировать набор глав без явной декларации массива или вектора. Такой вариант дополнительно привлекателен тем, что никакого временного вектора создаваться не будет. std::initializer_list является легковесной оберткой, реализуемой при помощи двух указателей, а сам std::vector содержит конструктор, принимающий такой список инициализаторов. Соответственно, помимо повышения уровня абстракции, улучшится и быстродействие, т.к. понадобится лишь 1 объект-вектор, который ни разу не подвергнется копированию на протяжении всей программы.

 

Следующий шаг должен состоять в пересмотре логики доступа к набору. Вместо возврата ссылки на вектор, непосредственную структуру данных для хранения, для организации простого перебора элементов подойдет пара методов, возвращающих количество (getChaptersCount) и конкретный дочерний объект по передаваемому порядковому номеру (getChapter).

 

Ниже приведен набросок такого улучшенного решения:

 

class Book

{

 

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

 

public:

 

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

 

// Конструктор

Book (std::string const & _title)

: m_title(_title)

{}

 

// Конструктор со списком глав

Book (std::string const & _title, std::initializer_list< Chapter * > _chapters)

: m_title(_title), m_chapters(_chapters)

{}

 

// Запрещенные конструктор копий и оператор присвоения

Book (const Book &) = delete;

Book & operator = (const Book &) = delete;

 

// Деструктор

~Book ()

{

for (Chapter * pChapter: m_chapters)

delete pChapter;

}

 

// Метод доступа к названию главы

std::string const & getTitle () const { return m_title; }

 

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

int getChaptersCount () const { return m_chapters.size(); }

 

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

Chapter & getChapter (int _index) const

{

return * m_chapters.at(_index);

}

 

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

void addChapter (Chapter * _pChapter)

{

m_chapters.push_back(_pChapter);



Поделиться:


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

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