16.3 – Агрегация (агрегирование по ссылке)
В предыдущем уроке «16.2 – Композиция (агрегирование по значению)» мы отметили, что композиция объектов – это процесс создания сложных объектов из более простых. Мы также обсудили один из типов композиции объектов, называемый композицией. В связях композиции родительский объект отвечает за существование дочернего объекта.
В этом уроке мы рассмотрим другой подтип композиции объектов, называемый агрегацией.
Агрегация
Чтобы квалифицироваться как агрегация, родительский объект и его компоненты должны иметь следующие связи:
- часть (член) является частью объекта (класса);
- часть (член) может принадлежать более чем одному объекту (классу) одновременно;
- существование части (члена) не управляется объектом (классом);
- часть (член) не знает о существовании объекта (класса).
Как и композиция, агрегация по-прежнему является отношением «часть-целое», где части содержатся внутри целого, и это однонаправленная связь. Однако, в отличие от композиции, части могут принадлежать более чем одному объекту одновременно, и родительский объект не несет ответственности за существование и время жизни дочерних объектов. При создании агрегация не отвечает за создание частей. Когда агрегация уничтожается, она не несет ответственность за уничтожение частей.
Например, рассмотрим связь между человеком и его домашним адресом. В этом примере для простоты мы скажем, что у каждого человека есть адрес. Однако этот адрес может принадлежать более чем одному человеку одновременно: например, и вам, и вашему соседу по комнате или другому человеку. Однако этот адрес не управляется человеком – адрес, вероятно, существовал до того, как человек пришел туда, и будет существовать после того, как человек уйдет. Кроме того, человек знает, по какому адресу он живет, но адреса не знают, какие там живут люди. Следовательно, это связь агрегации.
Как вариант, рассмотрим автомобиль и двигатель. Автомобильный двигатель – это часть автомобиля. И хотя двигатель принадлежит автомобилю, он может принадлежать и другим вещам, например человеку, которому принадлежит машина. Автомобиль не несет ответственности за создание или уничтожение двигателя. И хотя автомобиль знает, что у него есть двигатель (он ведь должен как-то передвигаться), двигатель не знает, что он часть автомобиля.
Когда дело доходит до моделирования физических объектов, использование термина «уничтожается» может быть немного рискованным. Кто-то может возразить: «Если бы метеорит упал с неба и раздавил автомобиль, разве не будут уничтожены все детали автомобиля?» Да, конечно. Но это вина метеорита. Важным моментом является то, что автомобиль не несет ответственности за уничтожение своих частей (но это может быть внешняя сила).
Можно сказать, что модели агрегации имеют отношения «есть» (на факультете есть преподаватели, у автомобиля есть двигатель).
Подобно композиции, компоненты агрегации могут быть единственными или мультипликативными.
Реализация агрегаций
Поскольку агрегации похожи на композиции в том смысле, что они являются отношениями «часть-целое», они реализуются почти идентично, а разница между ними в основном семантическая. В композиции мы обычно добавляем наши части в композицию, используя обычные переменные-члены (или указатели, где процесс выделения и освобождения памяти обрабатывается классом композиции).
При агрегации мы также добавляем части как переменные-члены. Однако эти переменные-члены обычно являются либо ссылками, либо указателями, которые используются для указания на объекты, которые были созданы вне области видимости класса. Следовательно, агрегация обычно либо принимает объекты, на которые она будет указывать, как параметры конструктора, либо она начинает с пустого значения, а подобъекты добавляются позже через функции доступа или через операторы.
Поскольку части существуют вне области действия класса, при уничтожении класса указатель или ссылочная переменная-член будет уничтожена (но не удалена). Следовательно, сами части всё равно будут существовать.
Давайте рассмотрим пример преподавателя Teacher
и факультета Department
более подробно. В этом примере мы сделаем несколько упрощений: во-первых, на факультете будет только один преподаватель. Во-вторых, преподаватель не знает, на каком факультете он работает.
#include <iostream>
#include <string>
class Teacher
{
private:
std::string m_name{};
public:
Teacher(const std::string& name)
: m_name{ name }
{
}
const std::string& getName() const { return m_name; }
};
class Department
{
private:
// На этом факультете для простоты работает только один
// преподаватель, но на нем их может быть много
const Teacher& m_teacher;
public:
Department(const Teacher& teacher)
: m_teacher{ teacher }
{
}
};
int main()
{
// Создаем преподавателя вне факультета
Teacher bob{ "Bob" };
{
// Создаем факультет и используем параметр конструктора
// для передачи ему преподавателя.
Department department{ bob };
} // department здесь выходит из области видимости и уничтожается
// bob здесь всё еще существует, а department нет
std::cout << bob.getName() << " still exists!\n";
return 0;
}
В этом случае bob
создается независимо от department
, а затем передается в конструктор department
. Когда department
уничтожается, ссылка m_teacher
уничтожается, но сам объект преподавателя не уничтожается, поэтому он всё еще существует до тех пор, пока не будет независимо уничтожен позже в main()
.
Выбирайте правильную связь для того, что моделируете
Хотя в приведенном выше примере может показаться немного глупым, что преподаватели не знают, на каком факультете они работают, но это может быть совершенно нормально в контексте данной программы. Когда вы определяете, какие связи следует реализовать, реализуйте самые простые связи, которые соответствуют вашим потребностям, а не те, которые кажутся наиболее подходящими в реальной жизни.
Например, если вы пишете симулятор автомастерской, вы можете реализовать автомобиль и двигатель как агрегацию, чтобы в будущем двигатель можно было снять и положить на полку. Однако если вы пишете симулятор гонок, то можете реализовать автомобиль и двигатель как композицию, поскольку в этом контексте двигатель никогда не будет существовать вне автомобиля.
Правило
Реализуйте простейший тип связи, который соответствует потребностям вашей программы, а не тот, что кажется правильным в реальной жизни.
Резюмируя информацию о композиции и агрегации
Композиции:
- обычно используют обычные переменные-члены;
- могут использовать члены-указатели, если класс сам обрабатывает размещение/освобождение объекта;
- ответственны за создание/уничтожение компонентов.
Агрегации:
- обычно используют члены-указатели или члены-ссылки, которые указывают или ссылаются на объекты, которые находятся за пределами области действия агрегирующего класса;
- не несут ответственности за создание/уничтожение компонентов.
Стоит отметить, что понятия композиции и агрегации можно свободно смешивать в рамках одного класса. Вполне возможно написать класс, который отвечает за создание/уничтожение одних частей, но не отвечает за создание/уничтожение других. Например, у нашего класса факультета Department
может быть название и преподаватель (Teacher
). Название, вероятно, будет добавлено к Department
как композиция и будет создаваться и уничтожаться вместе с объектом Department
. Teacher
, напротив, будет добавлен к Department
путем агрегации и будет создаваться/уничтожаться независимо.
Хотя агрегации могут быть чрезвычайно полезными, они также потенциально более опасны, потому что агрегации не обрабатывают удаление своих компонентов. Удаление остается на усмотрение внешней стороны. Если внешняя сторона больше не имеет указателя или ссылки на отброшенные компоненты, или если она просто забывает выполнить очистку (при условии, что класс может ее обработать), произойдет утечка памяти.
По этой причине следует отдавать предпочтение композициям, а не агрегациям.
Несколько предупреждений/исправлений
По ряду исторических и контекстных причин, в отличие от композиции, определение агрегации неточно, поэтому вы можете увидеть, что другие справочные материалы определяют его немного иначе, чем мы. Это нормально, просто имейте в виду.
Последнее замечание: в уроке «9.4 – Структуры» мы определили агрегированные (составные) типы данных (такие как структуры и классы) как типы данных, которые группируют вместе несколько переменных. Вы также можете встретить термин агрегированный класс в своем изучении C++, который определяется как структура или класс, который не имеет предоставленных конструкторов, деструкторов или перегруженного присваивания, все члены которого открыты, и который не использует наследование – по сути, простая структура данных. Несмотря на сходство в именовании, агрегированный класс и агрегация различаются, и их не следует путать.
std::reference_wrapper
В приведенном выше примере Department
/Teacher
мы использовали ссылку в Department
для хранения Teacher
. Это отлично работает, если есть только один объект Teacher
, но если у нас список преподавателей, скажем, std::vector
, мы больше не можем использовать ссылки.
std::vector<const Teacher&> m_teachers{}; // недопустимо
Элементы списка не могут быть ссылками, потому что ссылки должны быть инициализированы и не могут быть переназначены. Вместо ссылок мы могли бы использовать указатели, но это открыло бы возможность хранить или передавать нулевые указатели. В примере Department
/Teacher
мы не хотим разрешать использование нулевых указателей. Чтобы решить эту проблему, существует std::reference_wrapper
.
По сути, std::reference_wrapper
– это класс, который действует как ссылка, но также он позволяет выполнять присваивание и копирование, и поэтому он совместим со списками, такими как std::vector
.
Хорошая новость в том, что вам не нужно понимать, как он работает, чтобы его использовать. Всё, что вам нужно знать, это три вещи:
std::reference_wrapper
живет в заголовке<functional>
;- когда вы создаете объект, обернутый
std::reference_wrapper
, этот объект не может быть анонимным объектом (поскольку анонимные объекты имеют область видимости выражения и оставят ссылку висячей); - если вы хотите вернуть свой объект из
std::reference_wrapper
, используйте функцию-членget()
.
Вот пример использования std::reference_wrapper
в std::vector
:
#include <functional> // std::reference_wrapper
#include <iostream>
#include <vector>
#include <string>
int main()
{
std::string tom{ "Tom" };
std::string berta{ "Berta" };
std::vector<std::reference_wrapper<std::string>> names{ tom, berta };
std::string jim{ "Jim" };
names.push_back(jim);
for (auto name : names)
{
// Используйте функцию-член get(), чтобы получить строку.
name.get() += " Beam";
}
std::cout << jim << '\n'; // Jim Beam
return 0;
}
Чтобы создать вектор константных ссылок, нам нужно было бы добавить const
перед std::string
, вот так
// Вектор константных ссылок на std::string
std::vector<std::reference_wrapper<const std::string>> names{ tom, berta };
Если на данном этапе это кажется немного странным или непонятным (особенно в отношении вложенных типов), вернитесь к этому позже, после того как мы рассмотрим шаблоны классов, и вам, вероятно, всё будет более понятно.
Небольшой тест
Вопрос 1
Как вы реализовали бы следующие примеры (как композицию или как агрегацию)?
- мяч, имеющий цвет;
- работодатель, нанимающий несколько человек;
- факультеты в университете;
- ваш возраст;
- мешочек с шариками.
Ответ
- композиция: цвет – неотъемлемое свойство мяча;
- агрегация: работодатель начинает без сотрудников и, надеюсь, не уничтожает всех своих сотрудников, когда становится банкротом;
- композиция: факультеты не могут существовать без университета;
- композиция: ваш возраст является вашим собственным свойством;
- агрегация: мешок и шарики внутри существуют независимо друг от друга.
Вопрос 2
Обновите пример Department
/Teacher
, чтобы факультет Department
мог работать с несколькими преподавателями. Должен выполняться следующий код:
#include <iostream>
// ...
int main()
{
// Создаем преподавателя вне области видимости факультета
Teacher t1{ "Bob" };
Teacher t2{ "Frank" };
Teacher t3{ "Beth" };
{
// Создаем факультет и добавляем в него преподавателей
Department department{}; // создаем пустой объект Department
department.add(t1);
department.add(t2);
department.add(t3);
std::cout << department;
} // department здесь выходит из области видимости и уничтожается
std::cout << t1.getName() << " still exists!\n";
std::cout << t2.getName() << " still exists!\n";
std::cout << t3.getName() << " still exists!\n";
return 0;
}
Этот код должен напечатать:
Department: Bob Frank Beth
Bob still exists!
Frank still exists!
Beth still exists!
Подсказка
Храните преподавателей в
std::vector
std::vector<std::reference_wrapper<const Teacher>> m_teachers{};
Ответ
#include <functional> // std::reference_wrapper #include <iostream> #include <string> #include <vector> class Teacher { private: std::string m_name{}; public: Teacher(const std::string& name) : m_name{ name } { } const std::string& getName() const { return m_name; } }; class Department { private: std::vector<std::reference_wrapper<const Teacher>> m_teachers{}; public: // Передаем по обычной ссылке. Пользователю // класса Department не важно, как он реализован. void add(const Teacher& teacher) { m_teachers.push_back(teacher); } friend std::ostream& operator<<(std::ostream& out, const Department& department) { out << "Department: "; for (const auto& teacher : department.m_teachers) { out << teacher.get().getName() << ' '; } out << '\n'; return out; } }; int main() { // Создаем преподавателя вне области видимости факультета Teacher t1{ "Bob" }; Teacher t2{ "Frank" }; Teacher t3{ "Beth" }; { // Создаем факультет и добавляем в него преподавателей Department department{}; // создаем пустой объект Department department.add(t1); department.add(t2); department.add(t3); std::cout << department; } // department здесь выходит из области видимости и уничтожается std::cout << t1.getName() << " still exists!\n"; std::cout << t2.getName() << " still exists!\n"; std::cout << t3.getName() << " still exists!\n"; return 0; }