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

后端 未结 16 1787
忘掉有多难
忘掉有多难 2021-02-02 06:50

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 eac

相关标签:
16条回答
  • 2021-02-02 07:07

    You can at the cost of breaking good practices and maintaining unsafe code. Other answers will provide you with nasty tricks to achieve this.

    I dont like answers that just says "you should not do that", but I would like to suggest there probably is a better way to achieve the result you seek for.

    The strategy pattern as suggested in a comment by @manni66 is a good one.

    You should also think about data oriented design, since a class hierarchy does not look like a wise choice in your case.

    0 讨论(0)
  • 2021-02-02 07:07

    I have 2 solutions. A simpler one that doesn't preserve the memory address, and one that does preserve the memory address.

    Both require that you provide provide downcasts from Base to Derived which isn't a problem in your case.

    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{b.a} {};
      auto foo() -> void override { cout << "D1 " << a << endl; }
    };
    struct D2 : Base {
      using Base::Base;
      D2(Base b) : Base{b.a} {};
      auto foo() -> void override { cout << "D2 " << a << endl; }
    };
    

    For the former one you can create a smart pointer that can seemingly change the held data between Derived (and base) classes:

    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
    }
    

    The magic is in

    m.morph<D2>();
    

    which changes the held object preserving the data members (actually uses the cast ctor).


    If you need to preserve the memory location, you can adapt the above to use a buffer and placement new instead of unique_ptr. It is a little more work a whole lot more attention to pay to, but it gives you exactly what you need:

    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
    }
    

    This is of course the absolute bare bone. You can add move ctor, dereference operator etc.

    0 讨论(0)
  • 2021-02-02 07:08

    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.

    0 讨论(0)
  • 2021-02-02 07:09

    Yes and no. A C++ class defines the type of a memory region that is an object. Once the memory region has been instantiated, its type is set. You can try to work around the type system sure, but the compiler won't let you get away with it. Sooner or later it will shoot you in the foot, because the compiler made an assumption about types that you violated, and there is no way to stop the compiler from making such assumption in a portable fashion.

    However there is a design pattern for this: It's "State". You extract what changes into it's own class hierarchy, with its own base class, and you have your objects store a pointer to the abstract state base of this new hierarchy. You can then swap those to your hearts content.

    0 讨论(0)
  • 2021-02-02 07:09

    You can do what you're literally asking for with placement new and an explicit destructor call. Something like this:

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

    However as matb said, this really isn't a good design. I would recommend reconsidering what you're trying to do. Some of other answers here might also solve your problem, but I think anything along the idea of what you're asking for is going to be kludge. You should seriously consider designing your application so you can change the pointer when the type of the object changes.

    0 讨论(0)
  • 2021-02-02 07:12

    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.

    0 讨论(0)
提交回复
热议问题