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 15:07

In addition to other answers, you could use function pointers (or any wrapper on them, like std::function) to achieve the necessary bevahior:

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

Also, such function pointers approach would allow you to change any of the functions of the objects in run-time, not limiting you to some already defined sets of members implemented in derived classes.

查看更多
Anthone
3楼-- · 2019-03-11 15: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.

查看更多
放我归山
4楼-- · 2019-03-11 15:13

There is a simple error in your program. You assign the objects, but not the pointers:

int main() {
    Base* object = new Derived; //assign a new Derived class instance
    object->whoami(); //this prints "I am Derived"

    Base baseObject;

Now you assign baseObject to *object which overwrites the Derived object with a Base object. However, this does work well because you are overwriting an object of type Derived with an object of type Base. The default assignment operator just assigns all members, which in this case does nothing. The object cannot change its type and still is a Derived objects afterwards. In general, this can leads to serious problems e.g. object slicing.

    *object = baseObject; //reassign existing object to a different type
    object->whoami(); //but it *STILL* prints "I am Derived" (!)

    return 0;
}

If you instead just assign the pointer it will work as expected, but you just have two objects, one of type Derived and one Base, but I think you want some more dynamic behavior. It sounds like you could implement the specialness as a Decorator.

You have a base-class with some operation, and several derived classes that change/modify/extend the base-class behavior of that operation. Since it is based on composition it can be changed dynamically. The trick is to store a base-class reference in the Decorator instances and use that for all other functionality.

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

There are alternative patterns like Strategy which implement different use cases resp. solve different problems. It would probably good to read the pattern documentation with special focus to the Intent and Motivation sections.

查看更多
我命由我不由天
5楼-- · 2019-03-11 15:13

DISCLAIMER: The code here is provided as means to understand an idea, not to be implemented in production.

You're using inheritance. It can achieve 3 things:

  • Add fields
  • Add methods
  • replace virtual methods

Out of all those features, you're using only the last one. This means that you're not actually forced to rely on inheritance. You can get the same results by many other means. The simplest is to keep tabs on the "type" by yourself - this will allow you to change it on the fly:

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

};

Without inheritance the "type" is yours to do whatever you want. You can changeType at any time it suits you. With that power also comes responsibility: the compiler will no longer make sure the type is correct or even set at all. You have to ensure it or you'll get hard to debug runtime errors.

You may wrap it in inheritance just as well, eg. to get a drop-in replacement for existing code:

class Base : Any {
public:
    Base() : Any(BASE) {}
};

class Derived : public Any {
public:
    Derived() : Any(DERIVED) {}
};

OR (slightly uglier):

class Derived : public Base {
public:
    Derived : Base() {
        changeType(DERIVED)
    }
};

This solution is easy to implement and easy to understand. But with more options in the switch and more code in each path it gets very messy. So the very first step is to refactor the actual code out of the switch and into self-contained functions. Where better to keep than other than Derivied class?

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

Then you can replace the switch with an external class that implements it via virtual inheritance and TADA! We've reinvented the Strategy Pattern, as others have said in the first place : )

The bottom line is: whatever you do, you're not inheriting the main class.

查看更多
登录 后发表回答