如何在不邀请未来对象切片的情况下实现 ICloneable
How to implement ICloneable without inviting future object-slicing
我的问题是关于如何实现经典的 ICloneable
接口,以便在未来的程序员没有密切注意时不会导致无意的对象切片。这是我想检测的编程错误类型的示例(最好是在编译时):
#include <stdio.h>
class ICloneable
{
public:
virtual ICloneable * clone() const = 0;
};
class A : public ICloneable
{
public:
A() {}
A(const A & rhs) {}
virtual ICloneable * clone() const {return new A(*this);}
};
class B : public A
{
public:
B() {}
B(const B & rhs) {}
// Problem, B's programmer forget to add a clone() method here!
};
int main(int, char**)
{
B b;
ICloneable * clone = b.clone(); // d'oh! (clone) points to an A, not a B!
return 0;
}
如果 B
(或 B
的任何其他非抽象子类)没有定义自己的 [=14=,C++ 中是否有任何方法可以说服编译器发出错误] 方法?除此之外,是否有任何自动方法可以在 运行 时检测到此错误?
不久前,我在同样的情况下遇到了同样的问题,但没有找到令人满意的解决方案。
再次考虑这个问题,我发现了一些可能是解决方案(充其量)的方法:
#include <iostream>
#include <typeinfo>
#include <typeindex>
class Base { // abstract
protected:
Base() = default;
Base(const Base&) = default;
Base& operator=(const Base&) = default;
public:
virtual ~Base() = default;
Base* clone() const
{
Base *pClone = this->onClone();
const std::type_info &tiClone = typeid(*pClone);
const std::type_info &tiThis = typeid(*this);
#if 0 // in production
assert(std::type_index(tiClone) == type_index(tiThis)
&& "Missing overload of onClone()!");
#else // for demo
if (std::type_index(tiClone) != std::type_index(tiThis)) {
std::cout << "ERROR: Missing overload of onClone()!\n"
<< " in " << tiThis.name() << '\n';
}
#endif // 0
return pClone;
}
protected:
virtual Base* onClone() const = 0;
};
class Instanceable: public Base {
public:
Instanceable() = default;
Instanceable(const Instanceable&) = default;
Instanceable& operator=(const Instanceable&) = default;
virtual ~Instanceable() = default;
protected:
virtual Base* onClone() const { return new Instanceable(*this); }
};
class Derived: public Instanceable {
public:
Derived() = default;
Derived(const Derived&) = default;
Derived& operator=(const Derived&) = default;
virtual ~Derived() = default;
protected:
virtual Base* onClone() const override { return new Derived(*this); }
};
class WrongDerived: public Derived {
public:
WrongDerived() = default;
WrongDerived(const WrongDerived&) = default;
WrongDerived& operator=(const WrongDerived&) = default;
virtual ~WrongDerived() = default;
// override missing
};
class BadDerived: public Derived {
public:
BadDerived() = default;
BadDerived(const BadDerived&) = default;
BadDerived& operator=(const BadDerived&) = default;
virtual ~BadDerived() = default;
// copy/paste error
protected:
virtual Base* onClone() const override { return new Derived(*this); }
};
#define DEBUG(...) std::cout << #__VA_ARGS__ << ";\n"; __VA_ARGS__
int main()
{
DEBUG(Instanceable obj1);
DEBUG(Base *pObj1Clone = obj1.clone());
DEBUG(std::cout << "-> " << typeid(*pObj1Clone).name() << "\n\n");
DEBUG(Derived obj2);
DEBUG(Base *pObj2Clone = obj2.clone());
DEBUG(std::cout << "-> " << typeid(*pObj2Clone).name() << "\n\n");
DEBUG(WrongDerived obj3);
DEBUG(Base *pObj3Clone = obj3.clone());
DEBUG(std::cout << "-> " << typeid(*pObj3Clone).name() << "\n\n");
DEBUG(BadDerived obj4);
DEBUG(Base *pObj4Clone = obj4.clone());
DEBUG(std::cout << "-> " << typeid(*pObj4Clone).name() << "\n\n");
}
输出:
Instanceable obj1;
Base *pObj1Clone = obj1.clone();
std::cout << "-> " << typeid(*pObj1Clone).name() << '\n';
-> 12Instanceable
Derived obj2;
Base *pObj2Clone = obj2.clone();
std::cout << "-> " << typeid(*pObj2Clone).name() << '\n';
-> 7Derived
WrongDerived obj3;
Base *pObj3Clone = obj3.clone();
ERROR: Missing overload of onClone()!
in 12WrongDerived
std::cout << "-> " << typeid(*pObj3Clone).name() << '\n';
-> 7Derived
BadDerived obj4;
Base *pObj4Clone = obj4.clone();
ERROR: Missing overload of onClone()!
in 10BadDerived
std::cout << "-> " << typeid(*pObj4Clone).name() << '\n';
-> 7Derived
技巧其实很简单:
它不是覆盖 clone()
本身,而是用作 virtual onClone()
方法的蹦床。因此,clone()
可以在返回之前检查结果的正确性。
这不是编译时检查,而是 运行 时检查(我认为这是第二好的选择)。假设开发中的 class 库的每个 class 都应该至少在开发过程中被检查/调试,我发现这非常可靠。
的公认答案向我展示了一种方法,使它更能避免 copy/paste 错误:
不是在每个覆盖的 clone()
中键入 class 名称,而是通过 decltype()
:
获得 class 名称
class Instanceable: public Base {
public:
Instanceable() = default;
Instanceable(const Instanceable&) = default;
Instanceable& operator=(const Instanceable&) = default;
virtual ~Instanceable() = default;
protected:
virtual Base* onClone() const
{
return new std::remove_const_t<std::remove_pointer_t<decltype(this)>>(*this);
}
};
class Derived: public Instanceable {
public:
Derived() = default;
Derived(const Derived&) = default;
Derived& operator=(const Derived&) = default;
virtual ~Derived() = default;
protected:
virtual Base* onClone() const override
{
return new std::remove_const_t<std::remove_pointer_t<decltype(this)>>(*this);
}
};
不要让 A 和 B 继承自 IClonable。使用包装器 (BluePrint) 代替:
struct IClonable {
virtual ~IClonable() = default;
virtual IClonable * clone() const = 0;
};
template<typename T>
class BluePrint final : IClonable {
public:
explicit BluePrint(T * element) : element(element) {
}
IClonable * clone() const override {
T * copy = element->clone();
return new BluePrint(copy);
}
T * get() const {
return element;
}
private:
T * const element;
};
struct A {
A * clone() const;
};
struct B : A {
B * clone() const;
};
尽管如此,您将不得不稍微修改一下代码,因为此 returns 是包装器的克隆,而不是立即克隆要克隆的元素。话又说回来,我不知道你打算如何使用IClonable接口,所以我无法为你完成这个例子。
我的问题是关于如何实现经典的 ICloneable
接口,以便在未来的程序员没有密切注意时不会导致无意的对象切片。这是我想检测的编程错误类型的示例(最好是在编译时):
#include <stdio.h>
class ICloneable
{
public:
virtual ICloneable * clone() const = 0;
};
class A : public ICloneable
{
public:
A() {}
A(const A & rhs) {}
virtual ICloneable * clone() const {return new A(*this);}
};
class B : public A
{
public:
B() {}
B(const B & rhs) {}
// Problem, B's programmer forget to add a clone() method here!
};
int main(int, char**)
{
B b;
ICloneable * clone = b.clone(); // d'oh! (clone) points to an A, not a B!
return 0;
}
如果 B
(或 B
的任何其他非抽象子类)没有定义自己的 [=14=,C++ 中是否有任何方法可以说服编译器发出错误] 方法?除此之外,是否有任何自动方法可以在 运行 时检测到此错误?
不久前,我在同样的情况下遇到了同样的问题,但没有找到令人满意的解决方案。
再次考虑这个问题,我发现了一些可能是解决方案(充其量)的方法:
#include <iostream>
#include <typeinfo>
#include <typeindex>
class Base { // abstract
protected:
Base() = default;
Base(const Base&) = default;
Base& operator=(const Base&) = default;
public:
virtual ~Base() = default;
Base* clone() const
{
Base *pClone = this->onClone();
const std::type_info &tiClone = typeid(*pClone);
const std::type_info &tiThis = typeid(*this);
#if 0 // in production
assert(std::type_index(tiClone) == type_index(tiThis)
&& "Missing overload of onClone()!");
#else // for demo
if (std::type_index(tiClone) != std::type_index(tiThis)) {
std::cout << "ERROR: Missing overload of onClone()!\n"
<< " in " << tiThis.name() << '\n';
}
#endif // 0
return pClone;
}
protected:
virtual Base* onClone() const = 0;
};
class Instanceable: public Base {
public:
Instanceable() = default;
Instanceable(const Instanceable&) = default;
Instanceable& operator=(const Instanceable&) = default;
virtual ~Instanceable() = default;
protected:
virtual Base* onClone() const { return new Instanceable(*this); }
};
class Derived: public Instanceable {
public:
Derived() = default;
Derived(const Derived&) = default;
Derived& operator=(const Derived&) = default;
virtual ~Derived() = default;
protected:
virtual Base* onClone() const override { return new Derived(*this); }
};
class WrongDerived: public Derived {
public:
WrongDerived() = default;
WrongDerived(const WrongDerived&) = default;
WrongDerived& operator=(const WrongDerived&) = default;
virtual ~WrongDerived() = default;
// override missing
};
class BadDerived: public Derived {
public:
BadDerived() = default;
BadDerived(const BadDerived&) = default;
BadDerived& operator=(const BadDerived&) = default;
virtual ~BadDerived() = default;
// copy/paste error
protected:
virtual Base* onClone() const override { return new Derived(*this); }
};
#define DEBUG(...) std::cout << #__VA_ARGS__ << ";\n"; __VA_ARGS__
int main()
{
DEBUG(Instanceable obj1);
DEBUG(Base *pObj1Clone = obj1.clone());
DEBUG(std::cout << "-> " << typeid(*pObj1Clone).name() << "\n\n");
DEBUG(Derived obj2);
DEBUG(Base *pObj2Clone = obj2.clone());
DEBUG(std::cout << "-> " << typeid(*pObj2Clone).name() << "\n\n");
DEBUG(WrongDerived obj3);
DEBUG(Base *pObj3Clone = obj3.clone());
DEBUG(std::cout << "-> " << typeid(*pObj3Clone).name() << "\n\n");
DEBUG(BadDerived obj4);
DEBUG(Base *pObj4Clone = obj4.clone());
DEBUG(std::cout << "-> " << typeid(*pObj4Clone).name() << "\n\n");
}
输出:
Instanceable obj1;
Base *pObj1Clone = obj1.clone();
std::cout << "-> " << typeid(*pObj1Clone).name() << '\n';
-> 12Instanceable
Derived obj2;
Base *pObj2Clone = obj2.clone();
std::cout << "-> " << typeid(*pObj2Clone).name() << '\n';
-> 7Derived
WrongDerived obj3;
Base *pObj3Clone = obj3.clone();
ERROR: Missing overload of onClone()!
in 12WrongDerived
std::cout << "-> " << typeid(*pObj3Clone).name() << '\n';
-> 7Derived
BadDerived obj4;
Base *pObj4Clone = obj4.clone();
ERROR: Missing overload of onClone()!
in 10BadDerived
std::cout << "-> " << typeid(*pObj4Clone).name() << '\n';
-> 7Derived
技巧其实很简单:
它不是覆盖 clone()
本身,而是用作 virtual onClone()
方法的蹦床。因此,clone()
可以在返回之前检查结果的正确性。
这不是编译时检查,而是 运行 时检查(我认为这是第二好的选择)。假设开发中的 class 库的每个 class 都应该至少在开发过程中被检查/调试,我发现这非常可靠。
不是在每个覆盖的 clone()
中键入 class 名称,而是通过 decltype()
:
class Instanceable: public Base {
public:
Instanceable() = default;
Instanceable(const Instanceable&) = default;
Instanceable& operator=(const Instanceable&) = default;
virtual ~Instanceable() = default;
protected:
virtual Base* onClone() const
{
return new std::remove_const_t<std::remove_pointer_t<decltype(this)>>(*this);
}
};
class Derived: public Instanceable {
public:
Derived() = default;
Derived(const Derived&) = default;
Derived& operator=(const Derived&) = default;
virtual ~Derived() = default;
protected:
virtual Base* onClone() const override
{
return new std::remove_const_t<std::remove_pointer_t<decltype(this)>>(*this);
}
};
不要让 A 和 B 继承自 IClonable。使用包装器 (BluePrint) 代替:
struct IClonable {
virtual ~IClonable() = default;
virtual IClonable * clone() const = 0;
};
template<typename T>
class BluePrint final : IClonable {
public:
explicit BluePrint(T * element) : element(element) {
}
IClonable * clone() const override {
T * copy = element->clone();
return new BluePrint(copy);
}
T * get() const {
return element;
}
private:
T * const element;
};
struct A {
A * clone() const;
};
struct B : A {
B * clone() const;
};
尽管如此,您将不得不稍微修改一下代码,因为此 returns 是包装器的克隆,而不是立即克隆要克隆的元素。话又说回来,我不知道你打算如何使用IClonable接口,所以我无法为你完成这个例子。