В программировании на C++ часто используют два способа объединять объекты: агрегацию и композицию. Эти концепции отражают разные отношения между классами и влияют на жизненный цикл и ответственность за уничтожение связанных объектов.
История вопроса:
В объектно-ориентированном проектировании всегда было важно разделять зависимости между объектами. С появлением объектных языков (Smalltalk, C++, Java) стал вопрос: как лучше моделировать отношения "часть — целое". В C++ это стало особо актуально из-за ручного управления памятью и жизненным циклом объектов.
Проблема:
Ошибочный выбор между агрегацией и композицией приводит либо к утечкам памяти, либо к дублированию ресурсов, либо к ошибкам разрушения объектов. Также часто путают эти понятия.
Решение:
Пример кода:
// Композиция: class Engine {}; class Car { Engine engine; // Engine создается и уничтожается вместе с Car }; // Агрегация: class Person {}; class Team { std::vector<Person*> members; // Указывает на объекты Person, не владеет ими };
Ключевые особенности:
Если в классе-члене лежит указатель на объект, это всегда агрегация?
Нет! Если класс владеет этим указателем (например, через std::unique_ptr), это всё равно композиция. Тип связи определяется не типом поля, а ответственностью за жизненный цикл.
class House { std::unique_ptr<Room> room; // композиция, House владеет Room };
Может ли композиция реализовываться через ссылку или raw pointer?
Может — но только если объект создаётся и уничтожается владельцем, а ссылка или указатель используется для оптимизации. Однако гораздо лучше использовать объекты по значению или smart pointers для явного выражения владения.
Что произойдет, если в композиции объект-парт создаётся вне владельца и передается ему?
В таком случае возникает риск нарушения инвариантов композиции: если внешне созданный объект передан владельцу, и тот его уничтожает, а где-то ещё осталась на него сcылка — появится dangling pointer. Нужно строго определять права владения и ответственность за уничтожение в проекте.
Одна команда решила хранить все вложенные объекты через сырые указатели в контейнере и вручную уничтожать их в деструкторе. Всё работало, пока не изменили схему владения. В результате указатель был освобождён дважды, возник crash.
Плюсы:
Минусы:
Другая команда перешла на std::unique_ptr для всех реально владельческих связей, а не-owning использовала только в виде временных ссылок. Это явно выразило архитектуру.
Плюсы:
Минусы: