Is it possible to change a C++ object's class

2019-03-11 14:34发布

I have a bunch of classes which all inherit the same attributes from a common base class. The base class implements some virtual functions that work in general cases, whilst each subclass re-implements those virtual functions for a variety of special cases.

Here's the situation: I want the special-ness of these sub-classed objects to be expendable. Essentially, I would like to implement an expend() function which causes an object to lose its sub-class identity and revert to being a base-class instance with the general-case behaviours implemented in the base class.

I should note that the derived classes don't introduce any additional variables, so both the base and derived classes should be the same size in memory.

I'm open to destroying the old object and creating a new one, as long as I can create the new object at the same memory address, so existing pointers aren't broken.

The following attempt doesn't work, and produces some seemingly unexpected behaviour. What am I missing here?

#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;
}

16条回答
闹够了就滚
2楼-- · 2019-03-11 14:48

I don’t disagree with the advice that this isn’t a great design, but another safe way to do it is with a union that can hold any of the classes you want to switch between, since the standard guarantees it can safely hold any of them. Here’s a version that encapsulates all the details inside the union itself:

#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 = u.d; // By the standard, u, u.b and u.d have the same address.

    reference.whoami(); // Should say derived.
    u.toBase();
    reference.whoami(); // Should say base.

    return EXIT_SUCCESS;
}

A simpler way to get what you want is probably to keep a container of Base * and replace the items individually as needed with new and delete. (Still remember to declare your destructor virtual! That’s important with polymorphic classes, so you call the right destructor for that instance, not the base class’ destructor.) This might save you some extra bytes on instances of the smaller classes. You would need to play around with smart pointers to get safe automatic deletion, though. One advantage of unions over smart pointers to dynamic memory is that you don’t have to allocate or free any more objects on the heap, but can just re-use the memory you have.

查看更多
ゆ 、 Hurt°
3楼-- · 2019-03-11 14:48

you cannot change to the type of an object after instantiation, as you can see in your example you have a pointer to a Base class (of type base class) so this type is stuck to it until the end.

  • the base pointer can point to upper or down object doesn't mean changed its type:

    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.
    
  • pointers are much strong, flexible, powerful as much dangerous so you should handle them cautiously.

in your example I can write:

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

above it's a disaster assigning value to un-allocated pointer. the program will crash.

in your example you escaped the crash through the memory which was allocated at first:

object = new Derived;

it's never good idea to assign a value and not address of a subclass object to base class. however in built-in you can but consider this example:

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;

so the result as not as you guess.

查看更多
Summer. ? 凉城
4楼-- · 2019-03-11 14:49

I'm open to destroying the old object and creating a new one, as long as I can create the new object at the same memory address, so existing pointers aren't broken.

The C++ Standard explicitly addresses this idea in section 3.8 (Object Lifetime):

If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object <snip>

Oh wow, this is exactly what you wanted. But I didn't show the whole rule. Here's the rest:

if:

  • the storage for the new object exactly overlays the storage location which the original object occupied, and
  • the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and
  • the type of the original object is not const-qualified, and, if a class type, does not contain any non-static data member whose type is const-qualified or a reference type, and
  • the original object was a most derived object (1.8) of type T and the new object is a most derived object of type T (that is, they are not base class subobjects).

So your idea has been thought of by the language committee and specifically made illegal, including the sneaky workaround that "I have a base class subobject of the right type, I'll just make a new object in its place" which the last bullet point stops in its tracks.

You can replace an object with an object of a different type as @RossRidge's answer shows. Or you can replace an object and keep using pointers that existed before the replacement. But you cannot do both together.

However, like the famous quote: "Any problem in computer science can be solved by adding a layer of indirection" and that is true here too.

Instead of your suggested method

Derived d;
Base* p = &d;
new (p) Base();  // makes p invalid!  Plus problems when d's destructor is automatically called

You can do:

unique_ptr<Base> p = make_unique<Derived>();
p.reset(make_unique<Base>());

If you hide this pointer and slight-of-hand inside another class, you'll have the "design pattern" such as State or Strategy mentioned in other answers. But they all rely on one extra level of indirection.

查看更多
相关推荐>>
5楼-- · 2019-03-11 14:49

I suggest you use the Strategy Pattern, e.g.

#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;
}

This is a very flexible pattern that has at least a few benefits over trying to deal with the issue through inheritance:

  • You can easily change the behavior of Foo later on by implementing a new Announcer
  • Your Announcers (and your Foos) are easily unit tested
  • You can reuse your Announcers elsewhere int he code

I suggest you have a look at the age-old "Composition vs. Inheritance" debate (cf. https://www.thoughtworks.com/insights/blog/composition-vs-inheritance-how-choose)

ps. You've leaked a Derived in your original post! Have a look at std::unique_ptr if it is available.

查看更多
劫难
6楼-- · 2019-03-11 14:51

Your assignment only assigns member variables, not the pointer used for virtual member function calls. You can easily replace that with full memory copy:

//*object = baseObject; //this assignment was wrong
memcpy(object, &baseObject, sizeof(baseObject));

Note that much like your attempted assignment, this would replace member variables in *object with those of the newly constructed baseObject - probably not what you actually want, so you'll have to copy the original member variables to the new baseObject first, using either assignment operator or copy constructor before the memcpy, i.e.

Base baseObject = *object;

It is possible to copy just the virtual functions table pointer but that would rely on internal knowledge about how the compiler stores it so is not recommended.

If keeping the object at the same memory address is not crucial, a simpler and so better approach would be the opposite - construct a new base object and copy the original object's member variables over - i.e. use a copy constructor.

object = new Base(*object);

But you'll also have to delete the original object, so the above one-liner won't be enough - you need to remember the original pointer in another variable in order to delete it, etc. If you have multiple references to that original object you'll need to update them all, and sometimes this can be quite complicated. Then the memcpy way is better.

If some of the member variables themselves are pointers to objects that are created/deleted in the main object's constructor/destructor, or if they have a more specialized assignment operator or other custom logic, you'll have some more work on your hands, but for trivial member variables this should be good enough.

查看更多
做自己的国王
7楼-- · 2019-03-11 14:55

You can by introducing a variable to the base class, so the memory footprint stays the same. By setting the flag you force calling the derived or the base class implementation.

#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;
}
查看更多
登录 后发表回答