Можно ли клонировать полиморфный объект, не добавляя вручную переопределенный метод клонирования в каждый производный класс в C++?

Типичный шаблон, когда вы хотите скопировать полиморфный класс, - это добавить метод виртуального клона и реализовать его в каждом производном классе следующим образом:

Base* Derived::clone()
{
    return new Derived(*this);
}

Тогда в коде вызова вы можете:

Base *x = new Derived();
Base *y = x->clone();

Однако, если у вас есть производные классы 50+ и вы понимаете, что вам нужна полиморфная копия, утомительно копировать и вставлять метод клонирования в каждый из них. И это, по сути, шаблон, который работает вокруг языкового ограничения, которое вы должны указать фактическое имя для вызова конструктора.

Я не отслеживал новые функции в последних стандартах C++... Есть ли способ избежать этого в современном C++?

Ответ 1

Вы можете использовать этот общий код CRTP

template <class Derived, class Base>
struct Clonable : Base {
    virtual Base* do_clone() {
        return new Derived(*static_cast<Derived*>(this));
    }
    Derived* clone() { // not virtual
        return static_cast<Derived*>(do_clone());
    }

    using Base::Base;
};

struct empty {};
struct A : Clonable<A, empty> {};
struct B : Clonable<B, A> {};

При желании его можно обобщить на умные указатели и несколько баз.

Ответ 2

Вы можете использовать подход CRTP, но у него есть и другие недостатки:

struct Base {
    virtual Base* clone() const = 0;
};

template <typename Derived>
class BaseT : public Base {
    // ...
public:
    Base* clone() const override {
        return new Derived(*static_cast<Derived*>(this));
    }
};

Использование:

class DerivedA : public BaseT<DerivedA> {
};

Base *x = new DerivedA();
Base *y = x->clone();

Я не отслеживал новые функции в последних стандартах C++... Есть ли способ избежать этого в современном C++?

Этот прием доступен начиная со стандарта C++ 98.

Ответ 3

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

template <typename B>
class polymorphic_value {
public:
    template <typename D,
        std::enable_if_t<
            std::is_base_of<B, std::decay_t<D>>::value, int> = 0>
    explicit polymorphic_value(D&& value)
        : ptr{std::make_unique<derived_t<std::decay_t<D>>>(std::forward<D>(value))}
    {}

    polymorphic_value(polymorphic_value const& rhs)
        : ptr{rhs.ptr->clone()}
    {}

    B const& get() const { return ptr->get(); }

    B& get() {
        // Safe usage of const_cast, since the actual object is not const:
        return const_cast<B&>(ptr->get());
    }

private:
    struct base_t {
        virtual ~base_t() = default;
        virtual std::unique_ptr<B> clone() const = 0;
        // With more effort, this doesn't have to be a virtual function.
        // For example, rolling our own vtables would make that possible.
        virtual B const& get() const = 0;
    };

    template <typename D>
    struct derived_t final : public base_t {
        explicit derived_t(D const& d)
            : value{d}
        {}

        explicit derived_t(D&& d)
            : value{std::move(d)}
        {}

        std::unique_ptr<B> clone() const override {
            return std::make_unique<D>(value);
        }

        B const& get() const override {
            return value;
        }

        D value;
    };

    std::unique_ptr<base_t> ptr;
};

Для полной реализации, которая следует за предложением, см. Репозиторий jbcoe github.

Пример использования:

class Base {
public:
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    Derived() = default;
    Derived(Derived const&);
};

int main() {
    polymorphic_value<Base> it{Derived{}};
    auto const copy = it;
}

Живи на Годболте

Ответ 4

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

struct A: public Base
{
    Base* Clone() { return new std::remove_reference_t<decltype(*this)>(*this); }
};

Использование CRTP не поможет избежать повторного дублирования имени класса, так как вам нужно будет написать имя класса в параметрах шаблона для базы CRTP.

Ответ 5

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

template<class Derived>
Base* clone(const Base* b) {
    return new Derived(static_cast<const Derived*>(b));
}

void SampleUsage() {
    Base* b = new Derived;
    Base*(*cloner)(const Base*) = clone<Derived>;
    Base* copy = cloner(b);
}

Тип cloner не зависит от производного. Это как упрощенная функция std ::.

Ответ 6

Вы можете использовать CRTP чтобы добавить дополнительный слой между вашим производным классом и базой, который реализует ваш метод клонирования.

struct Base {
    virtual ~Base() = default;
    virtual Base* clone() = 0;
};

template <typename T>
struct Base_with_clone : Base {
    Base* clone() {
        return new T(*this);
    }
};

struct Derived : Base_with_clone<Derived> {};