在实例化之后是否可以更改C ++对象的类?
我有一堆类都从一个共同的基类inheritance相同的属性。 基类实现了一些在一般情况下工作的虚拟函数,而每个子类重新实现了各种特殊情况下的虚拟函数。
情况如下:我希望这些分类对象的特殊性是可以消耗的。 本质上,我想要实现一个expend()
函数,这个函数会导致一个对象失去它的子类标识,然后恢复成基类实例,并且在基类中实现一般的行为。
我应该注意到派生类不会引入任何额外的variables,所以基类和派生类在内存中应该是相同的大小。
我打算销毁旧的对象,并创build一个新的,只要我可以在同一个内存地址创build新的对象,所以现有的指针不会被打破。
以下尝试不起作用,并产生一些看似意外的行为。 我在这里错过了什么?
#include <iostream> class Base { public: virtual void whoami() { std::cout << "I am Base\n"; } }; class Derived : public Base { public: void whoami() { std::cout << "I am Derived\n"; } }; Base* object; int main() { object = new Derived; //assign a new Derived class instance object->whoami(); //this prints "I am Derived" Base baseObject; *object = baseObject; //reassign existing object to a different type object->whoami(); //but it *STILL* prints "I am Derived" (!) return 0; }
您可以以破坏良好的做法和维护不安全的代码为代价。 其他答案将为您提供肮脏的技巧来实现这一点。
我不喜欢只是说“你不应该这样做”的答案,但我想build议可能有一个更好的方法来实现你所寻求的结果。
@ manni66评论中提出的策略模式是一个很好的策略模式 。
你还应该考虑数据导向devise ,因为在你的情况下,类层次结构看起来不是一个明智的select。
是和不是。 C ++类定义了作为对象的内存区域的types。 一旦内存区域被实例化,其types被设置。 你可以尝试解决types系统,但编译器不会让你摆脱它。 迟早它会把你击倒在脚下,因为编译器对你所违反的types做了一个假设,而且没有办法阻止编译器以便携的方式做出这样的假设。
但是有一个devise模式:这是“国家”。 您可以使用自己的基类将自己的类层次结构中的更改提取出来,并且您的对象将存储指向此新层次结构的抽象状态库的指针。 然后你可以把这些交换到你的心中。
不,一旦实例化就不可能改变对象的types。
*object = baseObject;
不会改变object
的types ,只是调用一个编译器生成的赋值操作符。
如果你写的话,这将是一个不同的问题
object = new Base;
(记得自然地调用delete
;目前你的代码泄漏了一个对象)。
从C ++ 11开始,您可以将资源从一个对象移动到另一个对象; 看到
我打算销毁旧的对象,并创build一个新的,只要我可以在同一个内存地址创build新的对象,所以现有的指针不会被打破。
C ++标准在第3.8节(对象生存期)中明确地阐述了这个想法:
如果在对象的生命周期结束之后并且在重新使用或释放对象占用的存储之前,在原始对象占据的存储位置处创build新的对象,则指向原始对象的指针 ,引用原始对象,或者原始对象的名称将自动引用新对象,并且一旦新对象的生命周期已经开始, 就可以用来操作新对象 <snip>
哇,这正是你想要的。 但我没有显示整个规则。 剩下的是:
如果 :
- 新对象的存储正好覆盖原对象占用的存储位置
- 新对象与原始对象的types相同(忽略顶级cv限定符) ,并且
- 原始对象的types不是const限定的,并且如果类types不包含任何types为const限定的非静态数据成员或引用types,并且
- 原始对象是types
T
的最多派生对象(1.8),新对象是typesT
的最派生对象(也就是说,它们不是基类子对象)。
所以你的想法已经被语言委员会想到了,特别是非法的,包括偷偷摸摸的解决方法,“我有一个正确的types的基类子对象,我只是在它的地方做一个新的对象”,这是最后一个要点停在它的轨道上。
你可以用@ RossRidge的答案显示的不同types的对象replace一个对象。 或者你可以replace一个对象,并继续使用replace之前存在的指针。 但是你们不能一起做。
然而,就像这句名言:“计算机科学中的任何问题都可以通过增加一层间接寻址来解决” ,在这里也是如此。
而不是你build议的方法
Derived d; Base* p = &d; new (p) Base(); // makes p invalid! Plus problems when d's destructor is automatically called
你可以做:
unique_ptr<Base> p = make_unique<Derived>(); p.reset(make_unique<Base>());
如果你将这个指针隐藏在另一个类的内部,你将会看到其他答案中提到的“devise模式”,例如State或者Strategy。 但是他们都依赖于一个额外的间接水平。
你可以做你真正要求放置新的和明确的析构函数调用。 像这样的东西:
#include <iostream> #include <stdlib.h> class Base { public: virtual void whoami() { std::cout << "I am Base\n"; } }; class Derived : public Base { public: void whoami() { std::cout << "I am Derived\n"; } }; union Both { Base base; Derived derived; }; Base *object; int main() { Both *tmp = (Both *) malloc(sizeof(Both)); object = new(&tmp->base) Base; object->whoami(); Base baseObject; tmp = (Both *) object; tmp->base.Base::~Base(); new(&tmp->derived) Derived; object->whoami(); return 0; }
然而,正如matb所说,这确实不是一个好的devise。 我会build议重新考虑你想要做的事情。 这里的其他一些答案也可能解决你的问题,但我认为任何你想要什么的概念将成为kludge。 您应该认真考虑devise您的应用程序,以便在对象types更改时更改指针。
我build议你使用战略模式,例如
#include <iostream> class IAnnouncer { public: virtual ~IAnnouncer() { } virtual void whoami() = 0; }; class AnnouncerA : public IAnnouncer { public: void whoami() override { std::cout << "I am A\n"; } }; class AnnouncerB : public IAnnouncer { public: void whoami() override { std::cout << "I am B\n"; } }; class Foo { public: Foo(IAnnouncer *announcer) : announcer(announcer) { } void run() { // Do stuff if(nullptr != announcer) { announcer->whoami(); } // Do other stuff } void expend(IAnnouncer* announcer) { this->announcer = announcer; } private: IAnnouncer *announcer; }; int main() { AnnouncerA a; Foo foo(&a); foo.run(); // Ready to "expend" AnnouncerB b; foo.expend(&b); foo.run(); return 0; }
这是一个非常灵活的模式,与通过inheritance来处理这个问题至less有一些好处:
- 稍后通过实施新的播音员,您可以轻松更改Foo的行为
- 你的播音员(和你的Foos)很容易进行unit testing
- 您可以在代码中的其他地方重复使用您的播音员
我build议你看一下这个古老的“构成与inheritance”辩论(参见https://www.thoughtworks.com/insights/blog/composition-vs-inheritance-how-choose )
PS。 您已经在原始文章中泄露了Derived! 看看std :: unique_ptr是否可用。
你可以通过向基类引入一个variables,所以内存占用情况保持不变。 通过设置标志,您可以强制调用派生类或基类实现。
#include <iostream> class Base { public: Base() : m_useDerived(true) { } void setUseDerived(bool value) { m_useDerived = value; } void whoami() { m_useDerived ? whoamiImpl() : Base::whoamiImpl(); } protected: virtual void whoamiImpl() { std::cout << "I am Base\n"; } private: bool m_useDerived; }; class Derived : public Base { protected: void whoamiImpl() { std::cout << "I am Derived\n"; } }; Base* object; int main() { object = new Derived; //assign a new Derived class instance object->whoami(); //this prints "I am Derived" object->setUseDerived(false); object->whoami(); //should print "I am Base" return 0; }
除了其他的答案,你可以使用函数指针(或任何包装,如std::function
)来实现必要的优化:
void print_base(void) { cout << "This is base" << endl; } void print_derived(void) { cout << "This is derived" << endl; } class Base { public: void (*print)(void); Base() { print = print_base; } }; class Derived : public Base { public: Derived() { print = print_derived; } }; int main() { Base* b = new Derived(); b->print(); // prints "This is derived" *b = Base(); b->print(); // prints "This is base" return 0; }
另外,这样的函数指针方法可以让你在运行时更改对象的任何函数,而不是限制你在派生类中实现的一些已经定义的成员集合。
在你的程序中有一个简单的错误。 您分配的对象,但不是指针:
int main() { Base* object = new Derived; //assign a new Derived class instance object->whoami(); //this prints "I am Derived" Base baseObject;
现在,您将baseObject
分配给使用Base
对象覆盖Derived
对象的*object
对象。 不过,这样做确实很好,因为您正在使用Base
types的对象覆盖Derived
types的对象。 默认的赋值操作符只是分配所有的成员,在这种情况下什么都不做。 该对象不能改变它的types,然后仍然是Derived
对象。 一般来说,这会导致严重的问题,例如对象切片。
*object = baseObject; //reassign existing object to a different type object->whoami(); //but it *STILL* prints "I am Derived" (!) return 0; }
如果你只是分配指针,它将按预期工作,但你只有两个对象,一个是Derived
types和一个Base
types,但是我认为你需要更多的dynamic行为。 这听起来像你可以实现作为装饰者的特殊性。
你有一个具有一些操作的基类,以及一些改变/修改/扩展该操作的基类行为的派生类。 由于它是基于构图,所以可以dynamic改变。 窍门是在Decorator实例中存储基类引用,并将其用于所有其他function。
class Base { public: virtual void whoami() { std::cout << "I am Base\n"; } virtual void otherFunctionality() {} }; class Derived1 : public Base { public: Derived1(Base* base): m_base(base) {} virtual void whoami() override { std::cout << "I am Derived\n"; // maybe even call the base-class implementation // if you just want to add something } virtual void otherFunctionality() { base->otherFunctionality(); } private: Base* m_base; }; Base* object; int main() { Base baseObject; object = new Derived(&baseObject); //assign a new Derived class instance object->whoami(); //this prints "I am Derived" // undecorate delete object; object = &baseObject; object->whoami(); return 0; }
像Strategy这样的替代模式实现了不同的用例, 解决不同的问题。 这可能是很好的阅读模式文档,特别关注的意图和动机部分。
我会考虑规范你的types。
class Base { public: virtual void whoami() { std::cout << "Base\n"; } std::unique_ptr<Base> clone() const { return std::make_unique<Base>(*this); } virtual ~Base() {} }; class Derived: public Base { virtual void whoami() overload { std::cout << "Derived\n"; }; std::unique_ptr<Base> clone() const override { return std::make_unique<Derived>(*this); } public: ~Derived() {} }; struct Base_Value { private: std::unique_ptr<Base> pImpl; public: void whoami () { pImpl->whoami(); } template<class T, class...Args> void emplace( Args&&...args ) { pImpl = std::make_unique<T>(std::forward<Args>(args)...); } Base_Value()=default; Base_Value(Base_Value&&)=default; Base_Value& operator=(Base_Value&&)=default; Base_Value(Base_Value const&o) { if (o.pImpl) pImpl = o.pImpl->clone(); } Base_Value& operator=(Base_Value&& o) { auto tmp = std::move(o); swap( pImpl, tmp.pImpl ); return *this; } };
现在, Base_Value
在语义上是一种多态的值types。
Base_Value object; object.emplace<Derived>(); object.whoami(); object.emplace<Base>(); object.whoami();
你可以将一个Base_Value
实例包装在一个智能指针中,但是我不打扰。
我并不反对这样的build议,即这不是一个好的devise,但另一个安全的方法是使用一个可以容纳任何你想切换的类的工会,因为标准保证它可以安全的他们。 这是一个封装联合体内所有细节的版本:
#include <cassert> #include <cstdlib> #include <iostream> #include <new> #include <typeinfo> class Base { public: virtual void whoami() { std::cout << "I am Base\n"; } virtual ~Base() {} // Every base class with child classes that might be deleted through a pointer to the // base must have a virtual destructor! }; class Derived : public Base { public: void whoami() { std::cout << "I am Derived\n"; } // At most one member of any union may have a default member initializer in C++11, so: Derived(bool) : Base() {} }; union BorD { Base b; Derived d; // Initialize one member. BorD(void) : b() {} // These defaults are not used here. BorD( const BorD& ) : b() {} // No per-instance data to worry about! // Otherwise, this could get complicated. BorD& operator= (const BorD& x) // Boilerplate: { if ( this != &x ) { this->~BorD(); new(this) BorD(x); } return *this; } BorD( const Derived& x ) : d(x) {} // The constructor we use. // To destroy, be sure to call the base class' virtual destructor, // which works so long as every member derives from Base. ~BorD(void) { dynamic_cast<Base*>(&this->b)->~Base(); } Base& toBase(void) { // Sets the active member to b. Base* const p = dynamic_cast<Base*>(&b); assert(p); // The dynamic_cast cannot currently fail, but check anyway. if ( typeid(*p) != typeid(Base) ) { p->~Base(); // Call the virtual destructor. new(&b) Base; // Call the constructor. } return b; } }; int main(void) { BorD u(Derived{false}); Base& reference = ud; // By the standard, u, ub and ud have the same address. reference.whoami(); // Should say derived. u.toBase(); reference.whoami(); // Should say base. return EXIT_SUCCESS; }
一个简单的方法来获得你想要的可能是保留一个Base *
的容器,并根据需要单独replace项目与new
和delete
。 (还记得要声明你的析构函数是virtual
!这对于多态类是很重要的,所以你可以为这个实例调用正确的析构函数,而不是基类的析构函数。)这可以为更小的类实例节省一些额外的字节。 尽pipe如此,您仍然需要使用智能指针来获取安全的自动删除。 智能指针指向dynamic内存的一个优点是您不必在堆上分配或释放更多的对象,但可以重新使用您拥有的内存。
免责声明:这里的代码是为了理解一个想法而提供的,而不是在生产中实现的。
你正在使用inheritance。 它可以实现3件事情:
- 添加字段
- 添加方法
- replace虚拟方法
在所有这些function中,您只使用最后一个function。 这意味着你实际上并不需要依赖inheritance。 您可以通过其他方式获得相同的结果。 最简单的方法就是自己贴上“types”的标签 – 这样可以让你在运行中改变它:
#include <stdexcept> enum MyType { BASE, DERIVED }; class Any { private: enum MyType type; public: void whoami() { switch(type){ case BASE: std::cout << "I am Base\n"; return; case DERIVED: std::cout << "I am Derived\n"; return; } throw std::runtime_error( "undefined type" ); } void changeType(MyType newType){ //insert some checks if that kind of transition is legal type = newType; } Any(MyType initialType){ type = initialType; } };
没有inheritance的“types”是你的任何你想做的事情。 你可以在任何时候改变它适合你。 由于这种权力也是责任:编译器将不再确保types是正确的,甚至根本不设置。 你必须确保它,否则你将很难debugging运行时错误。
你也可以用inheritance来包装它,例如。 获取现有代码的替代scheme:
class Base : Any { public: Base() : Any(BASE) {} }; class Derived : public Any { public: Derived() : Any(DERIVED) {} };
OR(稍微丑陋):
class Derived : public Base { public: Derived : Base() { changeType(DERIVED) } };
这个解决scheme很容易实现,易于理解。 但是随着更多的选项和更多的代码在每个path它变得非常混乱。 所以,第一步就是将实际的代码从开关中重构成独立的函数。 哪里比Derivied
类更好地保持?
class Base { public: static whoami(Any* This){ std::cout << "I am Base\n"; } }; class Derived { public: static whoami(Any* This){ std::cout << "I am Derived\n"; } }; /*you know where it goes*/ switch(type){ case BASE: Base:whoami(this); return; case DERIVED: Derived:whoami(this); return; }
然后你可以用一个外部类来replace交换机,通过虚拟inheritance和TADA实现它! 正如其他人所说的,我们已经改变了战略模式:)
底线是:无论你做什么,你都不会inheritance主类。
你不能在实例化之后改变对象的types,就像你在你的例子中看到的那样,你有一个指向Base类(基类的types)的指针,所以这个types被卡住了,直到结束。
-
基指针可以指向上或下对象并不意味着改变其types:
Base* ptrBase; // pointer to base class (type) ptrBase = new Derived; // pointer of type base class `points to an object of derived class` Base theBase; ptrBase = &theBase; // not *ptrBase = theDerived: Base of type Base class points to base Object.
-
指针强大,灵活,function强大,所以你应该谨慎处理。
在你的例子中,我可以写:
Base* object; // pointer to base class just declared to point to garbage Base bObject; // object of class Base *object = bObject; // as you did in your code
高于这是一个灾难分配值未分配的指针。 该程序将崩溃。
在你的例子中,你通过首先分配的内存逃脱了崩溃:
object = new Derived;
将一个子类对象的value and not address
赋给基类value and not address
一个好主意。 但是在内置中,您可以考虑这个例子:
int* pInt = NULL; int* ptrC = new int[1]; ptrC[0] = 1; pInt = ptrC; for(int i = 0; i < 1; i++) cout << pInt[i] << ", "; cout << endl; int* ptrD = new int[3]; ptrD[0] = 5; ptrD[1] = 7; ptrD[2] = 77; *pInt = *ptrD; // copying values of ptrD to a pointer which point to an array of only one element! // the correct way: // pInt = ptrD; for(int i = 0; i < 3; i++) cout << pInt[i] << ", "; cout << endl;
所以结果不像你猜想的那样。
我有2个解决scheme。 一个简单的不保存内存地址,一个保存内存地址。
两者都要求您提供从基础派生的向下转换,这对您的情况不是问题。
struct Base { int a; Base(int a) : a{a} {}; virtual ~Base() = default; virtual auto foo() -> void { cout << "Base " << a << endl; } }; struct D1 : Base { using Base::Base; D1(Base b) : Base{ba} {}; auto foo() -> void override { cout << "D1 " << a << endl; } }; struct D2 : Base { using Base::Base; D2(Base b) : Base{ba} {}; auto foo() -> void override { cout << "D2 " << a << endl; } };
对于前者,您可以创build一个智能指针,它可以看似改变Derived(和base)类之间的数据:
template <class B> struct Morpher { std::unique_ptr<B> obj; template <class D> auto morph() { obj = std::make_unique<D>(*obj); } auto operator->() -> B* { return obj.get(); } }; int main() { Morpher<Base> m{std::make_unique<D1>(24)}; m->foo(); // D1 24 m.morph<D2>(); m->foo(); // D2 24 }
魔法在
m.morph<D2>();
这改变了保留数据成员的被保存对象(实际上使用了cast)。
如果需要保留内存位置 ,则可以使用缓冲区和放置位置来替代unique_ptr
。 这是一个更多的工作更多的关注付出,但它给你正是你所需要的:
template <class B> struct Morpher { std::aligned_storage_t<sizeof(B)> buffer_; B *obj_; template <class D> Morpher(const D &new_obj) : obj_{new (&buffer_) D{new_obj}} { static_assert(std::is_base_of<B, D>::value && sizeof(D) == sizeof(B) && alignof(D) == alignof(B)); } Morpher(const Morpher &) = delete; auto operator=(const Morpher &) = delete; ~Morpher() { obj_->~B(); } template <class D> auto morph() { static_assert(std::is_base_of<B, D>::value && sizeof(D) == sizeof(B) && alignof(D) == alignof(B)); obj_->~B(); obj_ = new (&buffer_) D{*obj_}; } auto operator-> () -> B * { return obj_; } }; int main() { Morpher<Base> m{D1{24}}; m->foo(); // D1 24 m.morph<D2>(); m->foo(); // D2 24 m.morph<Base>(); m->foo(); // Base 24 }
这当然是绝对的裸骨。 您可以添加移动计算器,解除引用操作符等
您的任务只分配成员variables,而不是用于虚拟成员函数调用的指针。 您可以轻松地用完整的内存副本replace它:
//*object = baseObject; //this assignment was wrong memcpy(object, &baseObject, sizeof(baseObject));
请注意,就像您尝试赋值一样,这会将*object
成员variablesreplace为新构造的baseObject
成员variables – 可能不是您真正想要的,所以您必须首先将原始成员variables复制到新的baseObject,在memcpy
之前的赋值操作符或拷贝构造函数,即
Base baseObject = *object;
有可能只复制虚拟函数表指针,但是依赖关于编译器如何存储它的内部知识,所以不build议这样做。
如果将对象保存在相同的内存地址并不重要,那么更简单和更好的方法是相反的 – 构造一个新的基础对象并复制原始对象的成员variables – 即使用复制构造函数。
object = new Base(*object);
但是你也必须删除原来的对象,所以上面的一行代码是不够的 – 你需要记住另一个variables中的原始指针,以便删除它,等等。如果你有多个引用该原始对象,你需要更新它们,有时这可能是相当复杂的。 那么memcpy
方式就更好了。
如果某些成员variables本身是指向在主对象的构造函数/析构函数中创build/删除的对象的指针,或者如果它们具有更专门化的赋值运算符或其他自定义逻辑,那么您将得到更多的工作。对于微不足道的成员variables,这应该足够好。