是否可以在不手动将重写的克隆方法添加到 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<T>
接口的上下文中听到这一点。由于 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<CRTP_BASE<Derived>>
所以您只需将编写类名的需要从“new”语句转移到CRTP 一。有帮助吗? ;) 我提到,正如 OP 在他的问题中提到的那样,他希望避免在每个新的派生类类型中命名派生类名称。
@Klaus 可以说添加<Derived>
比在类中添加整个克隆方法要少得多。确实是品味问题,但我会说它可能会很有帮助。
同意! only 到了 OP 无法避免使用 CRTP 再次给出派生类的名称的地步。仅此而已,因为它被OP特别提及。
@Klaus 没有特别提到,OP唯一提到的是他发现在每个派生类中添加克隆方法很乏味。这提供了另一种选择。以上是关于是否可以在不手动将重写的克隆方法添加到 C++ 中的每个派生类的情况下克隆多态对象?的主要内容,如果未能解决你的问题,请参考以下文章
如何在不首先克隆整个 repo 的情况下将文件添加到远程 Git repo (Github)