ПрограммированиеC++ Software Architect

Что такое агрегация (aggregation) и композиция (composition) в C++? Как они отличаются друг от друга и когда какой подход использовать?

Проходите собеседования с ИИ помощником Hintsage

Ответ.

В программировании на C++ часто используют два способа объединять объекты: агрегацию и композицию. Эти концепции отражают разные отношения между классами и влияют на жизненный цикл и ответственность за уничтожение связанных объектов.

История вопроса:

В объектно-ориентированном проектировании всегда было важно разделять зависимости между объектами. С появлением объектных языков (Smalltalk, C++, Java) стал вопрос: как лучше моделировать отношения "часть — целое". В C++ это стало особо актуально из-за ручного управления памятью и жизненным циклом объектов.

Проблема:

Ошибочный выбор между агрегацией и композицией приводит либо к утечкам памяти, либо к дублированию ресурсов, либо к ошибкам разрушения объектов. Также часто путают эти понятия.

Решение:

  • Композиция — это отношение, при котором объект владеет частью-объектом и отвечает за его создание/уничтожение. В C++ это обычно выражается членом-классом по значению или через unique_ptr.
  • Агрегация — это более слабая связь, часть-объект существует вне "целого", и ответственность за его жизненный цикл не лежит на владельце. Обычно реализуется через необ owning-ссылку (указатель/reference).

Пример кода:

// Композиция: class Engine {}; class Car { Engine engine; // Engine создается и уничтожается вместе с Car }; // Агрегация: class Person {}; class Team { std::vector<Person*> members; // Указывает на объекты Person, не владеет ими };

Ключевые особенности:

  • Композиция — сильная связь (part-of), владеет
  • Агрегация — слабая связь (uses), не владеет
  • Композиция автоматизирует управление памятью, агрегация требует осторожности и соглашений о владельцах

Вопросы с подвохом.

Если в классе-члене лежит указатель на объект, это всегда агрегация?

Нет! Если класс владеет этим указателем (например, через std::unique_ptr), это всё равно композиция. Тип связи определяется не типом поля, а ответственностью за жизненный цикл.

class House { std::unique_ptr<Room> room; // композиция, House владеет Room };

Может ли композиция реализовываться через ссылку или raw pointer?

Может — но только если объект создаётся и уничтожается владельцем, а ссылка или указатель используется для оптимизации. Однако гораздо лучше использовать объекты по значению или smart pointers для явного выражения владения.

Что произойдет, если в композиции объект-парт создаётся вне владельца и передается ему?

В таком случае возникает риск нарушения инвариантов композиции: если внешне созданный объект передан владельцу, и тот его уничтожает, а где-то ещё осталась на него сcылка — появится dangling pointer. Нужно строго определять права владения и ответственность за уничтожение в проекте.

Типовые ошибки и анти-паттерны

  • Смешивать понятия агрегации и композиции (например, хранить ненужные raw pointers, но пытаться уничтожать их в деструкторе владельца)
  • Использовать агрегацию там, где нужен строгий жизненный цикл (например, детали сложного объекта)
  • Не освобождать не-owning указатели

Пример из жизни

Негативный кейс

Одна команда решила хранить все вложенные объекты через сырые указатели в контейнере и вручную уничтожать их в деструкторе. Всё работало, пока не изменили схему владения. В результате указатель был освобождён дважды, возник crash.

Плюсы:

  • Гибкость архитектуры для некоторых вариантов (например, плавающих отношений между объектами)

Минусы:

  • Высокий риск ошибок с управлением памятью
  • Трудно поддерживать

Позитивный кейс

Другая команда перешла на std::unique_ptr для всех реально владельческих связей, а не-owning использовала только в виде временных ссылок. Это явно выразило архитектуру.

Плюсы:

  • Прозрачные и понятные отношения владения
  • Нет утечек или двойных освобождений

Минусы:

  • Не всегда возможна циклическая композиция
  • Иногда приходится дорабатывать протоколы общения между объектами