是否可以在不手动将重写的克隆方法添加到 C++ 中的每个派生类的情况下克隆多态对象?

Posted

技术标签:

【中文标题】是否可以在不手动将重写的克隆方法添加到 C++ 中的每个派生类的情况下克隆多态对象?【英文标题】:Is it possible to clone a polymorphic object without manually adding overridden clone method into each derived class in C++? 【发布时间】:2019-03-09 10:36:45 【问题描述】:

当您想要复制多态类时,典型的模式是添加一个虚拟克隆方法并在每个派生类中实现它,如下所示:

Base* Derived::clone()

    return new Derived(*this);

然后在调用代码中你可以:

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

但是,如果您有 50 多个派生类并意识到您需要多态复制,那么将克隆方法复制粘贴到每个派生类中会很繁琐。它本质上是一个样板文件,可以解决语言限制,您必须拼出实际名称才能调用构造函数。

我没有跟踪最近 C++ 标准中的新特性...有没有办法在现代 C++ 中避免这种情况?

【问题讨论】:

我不认为 c++ 支持,但也许你可以写一个简单的宏,比如GEN_CLONE(Derived) 我想这样的框架很难想象,即使对于地球上一些最聪明的 C++ ISO 团队来说也是如此。一种体面的克隆模式尚未出现在任何语言中。看看爪哇的狗晚餐。 CRTP 解决方案可能是最好的解决方案。这不是一个“XY”问题吗?为什么你真的需要克隆东西? 据我所知,这里的正常做法是对复制构造函数as std::function does 进行类型擦除。这当然是可能的,而且这种技术肯定在使用。 我个人认为任何解决方案至少与实际问题一样复杂。所以我会硬着头皮实现你的clone() 函数以及你所有的其他强制覆盖。此外,您的 clone() 函数应该返回 std::unique_ptr<Base> @Bathsheba 对于多态对象,如果你想完整地复制它们(而不是切片),你不能真正避免克隆。 【参考方案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> ;

如果需要,它可以推广到智能指针和多个基数。

【讨论】:

哇,我从未听说过这种模式。我会检查我是否可以用它来解决我的问题。 @Calmarius CRTP 主要是 C++ 模式。难怪你是否习惯了其他语言。 @Red.Wave:在 C++ 之外,它被称为“F-bounded polymorphism”。您将主要在 Java 的 Comparable&lt;T&gt; 接口的上下文中听到这一点。由于 C++ 模板与类型擦除有根本的不同,CRTP 和 F 有界多态性显然不是一回事,但在实践中它们倾向于解决相同类型的问题。 @Kevin 认为它在 C++ 之外并不常见。?? do_clone() 方法返回Base 的目的是什么? πάντα ῥεῖ 的答案对我来说似乎更明显正确【参考方案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 标准开始就可以使用了。

【讨论】:

此技巧从 C++98 开始可用,如果省略了 override 说明符(直到 C++11 才引入)。 Cloneable 可能是比Base 更好的名字【参考方案3】:

如果您可以控制传递多态类型的方式,请使用类型擦除。特别是,proposed 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)
        : ptrstd::make_unique<derived_t<std::decay_t<D>>>(std::forward<D>(value))
    

    polymorphic_value(polymorphic_value const& rhs)
        : ptrrhs.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)
            : valued
        

        explicit derived_t(D&& d)
            : valuestd::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's github repository。

示例用法:

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

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

int main() 
    polymorphic_value<Base> itDerived;
    auto const copy = it;

Live on Godbolt

【讨论】:

【参考方案4】:

您可能有一个类来存储多态对象以及要克隆的位置?连同你的多态对象,你可以存储一个函数指针来进行克隆:

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 的类型与 Derived 无关。它就像一个简化的 std::function。

【讨论】:

这其实是个好主意,也是类型擦除的基础。为了提高可用性,最好将Base*Base*(*)(const Base*) 函数指针包装在一个可以自动正确执行此操作的类中。【参考方案5】:

您至少可以通过从类本身的类型中获取类名来避免编写类名:

struct A: public Base

    Base* Clone()  return new std::remove_reference_t<decltype(*this)>(*this); 
;

使用 CRTP 将无助于避免再次重复类名,因为您必须在 CRTP 基础的模板参数中写入类名。

【讨论】:

【参考方案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> ;

【讨论】:

将类的命名从“新”转移到 CRTP 基类...有帮助吗? 您不必写“new ClassName”...而是写class Derived&lt;CRTP_BASE&lt;Derived&gt;&gt; 所以您只需将编写类名的需要从“new”语句转移到CRTP 一。有帮助吗? ;) 我提到,正如 OP 在他的问题中提到的那样,他希望避免在每个新的派生类类型中命名派生类名称。 @Klaus 可以说添加&lt;Derived&gt; 比在类中添加整个克隆方法要少得多。确实是品味问题,但我会说它可能会很有帮助。 同意! only 到了 OP 无法避免使用 CRTP 再次给出派生类的名称的地步。仅此而已,因为它被OP特别提及。 @Klaus 没有特别提到,OP唯一提到的是他发现在每个派生类中添加克隆方法很乏味。这提供了另一种选择。

以上是关于是否可以在不手动将重写的克隆方法添加到 C++ 中的每个派生类的情况下克隆多态对象?的主要内容,如果未能解决你的问题,请参考以下文章

是否可以在不重写历史记录的情况下精简 .git 存储库?

如何在不首先克隆整个 repo 的情况下将文件添加到远程 Git repo (Github)

是否可以在不添加服务参考的情况下访问 WCF 服务?

如何将 MFC 支持添加到现有的 Win32 C++ 项目?

如何在不克隆的情况下获取子数组

在不损失精度的情况下将双精度从 C++ 转移到 python