Паттерны

Prototype: клонирование объектов

Введение

Завершаем блок порождающих паттернов темой Prototype — паттерном, решающим задачу создания нового объекта путём копирования (клонирования) уже существующего, готового экземпляра-образца, а не созданием с нуля через конструктор, что особенно полезно, когда создание объекта «с нуля» дорого или сложно, либо когда точный класс объекта, который нужно скопировать, неизвестен заранее на этапе компиляции (типичный случай для полиморфных объектов через указатель/ссылку на базовый класс,).

Концепция

Prototype определяет виртуальный метод clone() в базовом, абстрактном классе, каждая производная реализация которого создаёт и возвращает копию ИМЕННО СВОЕГО конкретного типа — это решает проблему, неразрешимую через обычный конструктор копирования в полиморфном контексте: имея лишь указатель/ссылку на базовый тип, обычный конструктор копирования не способен создать копию реального, конкретного, более производного типа объекта (произошла бы потеря информации о реальном типе — slicing), тогда как виртуальный clone(), реализованный в каждом конкретном производном классе, корректно создаёт копию именно того конкретного типа, каким реально является объект.

Пример кода

#include <iostream>
#include <memory>
#include <vector>

class Shape
{
public:
    virtual std::unique_ptr<Shape> clone() const = 0; // КАЖДЫЙ производный класс реализует СВОЁ клонирование
    virtual void draw() const = 0;
    virtual ~Shape() = default;
};

class Circle : public Shape
{
public:
    Circle(double radius) : m_radius(radius) {}

    std::unique_ptr<Shape> clone() const override
    {
        return std::make_unique<Circle>(*this); // конструктор копирования Circle — известен ТОЧНЫЙ тип
    }

    void draw() const override { std::cout << "Круг радиусом " << m_radius << std::endl; }

private:
    double m_radius;
};

class Square : public Shape
{
public:
    Square(double side) : m_side(side) {}
    std::unique_ptr<Shape> clone() const override { return std::make_unique<Square>(*this); }
    void draw() const override { std::cout << "Квадрат со стороной " << m_side << std::endl; }

private:
    double m_side;
};

void demonstratePolymorphicClone(const Shape &prototype)
{
    auto copy = prototype.clone(); // НЕ ЗНАЕМ конкретный тип prototype здесь — но clone() даёт КОРРЕКТНУЮ копию
    copy->draw(); // выведет именно ТО, что соответствует реальному, исходному типу
}

int main()
{
    Circle original(5.0);
    demonstratePolymorphicClone(original); // "Круг радиусом 5"

    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(3.0));
    shapes.push_back(std::make_unique<Square>(2.0));

    std::vector<std::unique_ptr<Shape>> clonedShapes;
    for (const auto &shape : shapes) {
        clonedShapes.push_back(shape->clone()); // клонирование ВСЕЙ коллекции разнородных объектов
    }

    return 0;
}

Пояснения к коду

Circle::clone(), вызывающий std::make_unique<Circle>(*this), показывает суть решения — внутри метода clone() конкретного класса Circle компилятор точно знает, что *this имеет тип именно Circle (а не абстрактный Shape), и поэтому конструктор копирования Circle вызывается корректно, создавая полную, правильную копию именно этого конкретного типа. demonstratePolymorphicClone(const Shape &prototype) показывает реальную ценность паттерна — функция принимает лишь ссылку на абстрактный базовый тип Shape, не зная заранее, передан ли реально Circle или Square, но вызов prototype.clone() благодаря полиморфизму автоматически вызывает правильную, соответствующую реальному типу реализацию, что было бы невозможно через попытку использовать обычный конструктор копирования Shape напрямую (который, не будучи виртуальным по своей природе в C++, не мог бы корректно скопировать более производный тип через ссылку на базовый класс).

Подводные камни

  • Попытка использовать обычный конструктор копирования для копирования объекта через ссылку/указатель на базовый, полиморфный тип вместо явного clone() — это классическая проблема «срезания» (slicing): копирование через конструктор копирования базового класса создаёт объект ИМЕННО базового типа, теряя любые дополнительные поля и поведение реального, более производного типа, тогда как виртуальный clone() корректно решает именно эту проблему, явно создавая полную копию реального типа объекта.
  • Забытая реализация clone() в одном из производных классов, оставляющая этот класс абстрактным (аналогично подводному камню статьи цикла синтаксиса про забытое переопределение чисто виртуальной функции) — без явной реализации clone() для каждого конкретного производного класса попытка создать объект ЭТОГО класса (а не только вызвать clone()) станет ошибкой компиляции, что является прямым следствием того, что класс без полной реализации всех чисто виртуальных методов базового класса остаётся абстрактным.
  • Реализация clone(), возвращающего «сырой» указатель вместо умного указателя (статья 547 цикла синтаксиса), что переносит на вызывающий код ответственность за явное, корректное освобождение клонированного объекта — современная практика C++ настойчиво рекомендует именно std::unique_ptr<Shape> как тип возвращаемого значения clone() (как показано в примере), что автоматически и однозначно решает вопрос владения и освобождения клонированного объекта.
  • Игнорирование стоимости клонирования для объектов с дорогими в копировании внутренними данными (большие массивы, сложные вложенные структуры) — каждый вызов clone() выполняет полное копирование всех данных объекта, и для часто клонируемых, «тяжёлых» объектов эта стоимость может быть значительной, что стоит явно учитывать при проектировании архитектуры, интенсивно использующей Prototype, возможно рассматривая альтернативы (совместное использование неизменяемых частей данных через shared_ptr, вместо их полного копирования при каждом клонировании).