实例化后是否可以更改C ++对象的类?

时间:2016-12-19 10:03:37

标签: c++ inheritance

我有一堆类,它们都从公共基类继承相同的属性。基类实现了一些在一般情况下工作的虚函数,而每个子类为各种特殊情况重新实现了这些虚函数。

情况就是这样:我希望这些子类对象的特殊性是可以消耗的。本质上,我想实现一个expend()函数,它导致一个对象失去它的子类标识,并恢复为一个基类实例,并在基类中实现了一般情况行为。

我应该注意,派生类不会引入任何其他变量,因此基类和派生类在内存中的大小应该相同。

我愿意破坏旧对象并创建一个新对象,只要我可以在同一个内存地址创建新对象,那么现有的指针就不会被破坏。

以下尝试不起作用,并产生一些看似意外的行为。我在这里缺少什么?

#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 个答案:

答案 0 :(得分:35)

您可以以破坏良好做法和维护不安全代码为代价。其他答案将为您提供令人讨厌的技巧来实现这一目标。

我不喜欢只说“你不应该这样做”的答案,但我想建议可能有更好的方法来实现你所寻求的结果。

@ manni66评论中建议的strategy pattern是一个很好的。

您还应该考虑data oriented design,因为在您的情况下,类层次结构看起来不是明智的选择。

答案 1 :(得分:16)

是和否。 C ++类定义作为对象的内存区域的类型。一旦实例化了内存区域,就会设置其类型。你可以尝试在类型系统中使用 ,但是编译器不会让你逃脱它。迟早它会射击你的脚,因为编译器对你违反的类型做出了假设,并且没有办法阻止编译器以便携的方式做出这样的假设。

然而,有一种设计模式:它是“状态”。您可以使用自己的基类将更改提取到其自己的类层次结构中,并且您的对象存储指向此新层次结构的抽象状态库的指针。然后你可以将它们交换到心中。

答案 2 :(得分:14)

一旦实例化,就不可能改变对象的类型。

*object = baseObject;不会更改object类型,它只会调用编译器生成的赋值运算符。

如果你写了

,那将是另一回事

object = new Base;

(记得自然地调用delete;目前你的代码泄漏了一个对象。)

C ++ 11以后,您可以将资源从一个对象移动到另一个对象;见

http://en.cppreference.com/w/cpp/utility/move

答案 3 :(得分:13)

  

我愿意破坏旧对象并创建一个新对象,只要我可以在同一个内存地址创建新对象,那么现有的指针就不会被破坏。

C ++标准在3.8节(对象生命周期)中明确地提出了这个想法:

  

如果,在对象的生命周期结束之后,在重用或释放对象占用的存储之前,在原始对象占用的存储位置创建一个新对象,一个指向原始对象的指针,一个引用原始对象的引用,或者原始对象的名称将自动引用新对象,并且一旦新对象的生命周期开始,< strong>可用于操纵新对象&lt; snip&gt;

哇,哇,这正是你想要的。但我没有表现出整个规则。剩下的就是:

  

如果

     
      
  • 新对象的存储空间正好覆盖原始对象占用的存储位置,
  •   
  • 新对象与原始对象的类型相同(忽略顶级cv限定符)
  •   
  • 原始对象的类型不是const限定的,如果是类类型,则不包含任何类型为const-qualified或引用类型的非静态数据成员,并且
  •   
  • 原始对象是类型T的派生程度最高的对象(1.8),新对象是类型T的派生程度最高的对象(也就是说,它们不是基类子对象)。 / LI>   

所以你的想法被语言委员会想到并且特别是非法的,包括偷偷摸摸的解决方法“我有一个正确类型的基类子对象,我只是在它的位置创建一个新对象”,其中最后一个子弹点停在其轨道上。

您可以使用@ RossRidge的答案显示的不同类型的对象替换对象。或者您可以替换对象并继续使用替换前存在的指针。但你不能一起做两件事。

然而,就像the famous quote: "Any problem in computer science can be solved by adding a layer of indirection"一样,这里也是如此。

而不是您建议的方法

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>());

如果你将这个指针隐藏在另一个类中,那么你将拥有“设计模式”,例如其他答案中提到的State或Strategy。但它们都依赖于一个额外的间接层。

答案 4 :(得分:9)

您可以通过展示新的和明确的析构函数调用来实现您真正要求的内容。像这样:

#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所说,这真的不是一个好的设计。我建议重新考虑你要做的事情。这里的一些其他答案也可以解决你的问题,但我认为你所要求的任何想法都会成为问题。您应该认真考虑设计应用程序,以便在对象类型发生变化时更改指针。

答案 5 :(得分:9)

我建议您使用策略模式,例如

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

这是一种非常灵活的模式,与通过继承处理问题相比至少有一些好处:

  • 您可以稍后通过实施新的播音员
  • 轻松更改Foo的行为
  • 您的播音员(和您的Foos)很容易进行单元测试
  • 您可以在代码的其他地方重复使用您的播音员

我建议你看看古老的“构成与继承”辩论(参见https://www.thoughtworks.com/insights/blog/composition-vs-inheritance-how-choose

PS。你在原帖中漏了一个Derived!如果可用,请查看std :: unique_ptr。

答案 6 :(得分:8)

您可以通过向基类引入变量,因此内存占用量保持不变。通过设置标志,您可以强制调用派生或基类实现。

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

答案 7 :(得分:7)

除了其他答案之外,你可以使用函数指针(或它们上的任何包装器,如std::function)来实现必要的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;
}

此外,这样的函数指针方法允许您在运行时更改对象的任何函数,而不是限制您在派生类中实现的某些已定义的成员集。

答案 8 :(得分:3)

程序中有一个简单的错误。您分配对象,但不分配指针:

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

    Base baseObject;

现在您将baseObject分配给*object,用Derived对象覆盖Base对象。但是,这确实很有效,因为您使用类型为Derived的对象覆盖类型为Base的对象。默认赋值运算符只分配所有成员,在这种情况下不执行任何操作。对象无法更改其类型,之后仍然是Derived个对象。通常,这会导致严重的问题,例如对象切片。

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

    return 0;
}

如果您只是指定指针它将按预期工作,但您只有两个对象,一个类型Derived和一个Base,但我认为您需要一些更动态的行为。听起来你可以将特殊性作为Decorator来实现。

您有一个带有一些操作的基类,以及一些更改/修改/扩展该操作的基类行为的派生类。由于它基于组合,因此可以动态更改。诀窍是在Decorator实例中存储基类引用,并将其用于所有其他功能。

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这样的替代模式可以实现不同的用例。解决不同的问题。阅读模式文档可能会很好,特别关注Intent和Motivation部分。

答案 9 :(得分:3)

我会考虑规范你的类型。

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在语义上是一种多态行为的值类型。

Base_Value object;
object.emplace<Derived>();
object.whoami();

object.emplace<Base>();
object.whoami();

您可以将Base_Value实例包装在智能指针中,但我不会打扰。

答案 10 :(得分:1)

我不同意这不是一个伟大的设计的建议,但另一种安全的方法是使用一个可以容纳你想要切换的任何类的联合,因为标准保证它可以安全地握住它们这是一个封装了union本身内部所有细节的版本:

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

获得所需内容的简单方法可能是保留Base *容器,并根据需要使用newdelete单独替换项目。 (还记得声明你的析构函数virtual!这对于多态类很重要,所以你为该实例调用正确的析构函数,而不是基类的析构函数。)这可能会在较小的类的实例上节省一些额外的字节。但是,您需要使用智能指针来获得安全的自动删除。联合对智能指针到动态内存的一个优点是,您不必在堆上分配或释放任何更多对象,但可以重新使用您拥有的内存。

答案 11 :(得分:0)

免责声明:此处的代码是作为理解一个想法的手段提供的,而不是在生产中实现。

你正在使用继承。它可以实现三件事:

  • 添加字段
  • 添加方法
  • 替换虚拟方法

在所有这些功能中,您只使用最后一个功能。这意味着你实际上并没有被迫依赖继承。您可以通过许多其他方式获得相同的结果。最简单的是自己监控“类型” - 这将允许您动态更改它:

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

};

没有继承,“类型”是你自己做任何你想做的事情。您可以changeType随时适合您。有了这个功能也有责任:编译器将不再确保类型是正确的,甚至根本不设置。你必须确保它,否则你将很难调试运行时错误。

您也可以将它包装在继承中,例如。获得现有代码的直接替换:

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

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

或(稍微丑陋):

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

此解决方案易于实施且易于理解。但是,如果交换机中有更多选项,并且每个路径中的代码更多,则会变得非常混乱。因此,第一步是将实际代码重构为交换机并重构为自包含函数。哪个比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;
    }

然后你可以用一个通过虚拟继承和TADA实现它的外部类替换开关!我们已经彻底改变了战略模式,正如其他人首先所说的那样:)

底线是:无论你做什么,你都不会继承主类。

答案 12 :(得分:0)

在实例化之后你无法更改为对象的类型,正如你在示例中看到的那样,你有一个指向Base类(类型基类)的指针,所以这种类型一直坚持到最后。

  • 基指针可以指向上方或下方对象并不意味着改变了它的类型:

    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.
    
  • 指针非常强大,灵活,功能强大,所以你应该小心处理它们。

在你的例子中我可以写:

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子类对象分配给基类永远不是一个好主意。但是在内置中你可以考虑这个例子:

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;

所以结果并不像你猜的那样。

答案 13 :(得分:0)

我有2个解决方案。更简单的一个不保留内存地址,另一个保留内存地址。

两者都要求您提供从Base到Derived的向下转发,这在您的情况下不是问题。

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

对于前者,你可以创建一个智能指针,它可以看似改变派生(和基础)类之间的数据:

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>();

更改保留数据成员的保持对象(实际上使用强制转换器)。

如果您需要保留内存位置,则可以调整上述内容以使用缓冲区和展示位置而不是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
}

这当然是绝对的裸骨。您可以添加move ctor,dereference operator等。

答案 14 :(得分:-1)

 #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; 
    object->whoami(); 
    Base baseObject;
    object = &baseObject;// this is how you change.
    object->whoami();
    return 0;
}

输出:

I am Derived                                                                                                                     
I am Base 

答案 15 :(得分:-2)

您的作业只分配成员变量,而不是用于虚拟成员函数调用的指针。您可以使用完整内存副本轻松替换它:

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

请注意,与您尝试的分配非常相似,这会将*object中的成员变量替换为新构建的baseObject中的成员变量 - 可能不是您真正想要的,所以您必须复制首先使用赋值运算符或复制构造函数在memcpy之前将原始成员变量添加到新的baseObject,即

Base baseObject = *object;

可以只复制虚函数表指针,但这依赖于有关编译器如何存储它的内部知识,因此不建议这样做。

如果将对象保持在相同的内存地址并不重要,那么更简单且更好的方法则相反 - 构造一个新的基础对象并复制原始对象的成员变量 - 即使用复制构造函数

object = new Base(*object);

但是你还必须删除原始对象,所以上面的单行程不够 - 你需要记住另一个变量中的原始指针才能删除它等等。你有多个对原始对象的引用,你需要更新它们,有时候这可能非常复杂。然后memcpy方式更好。

如果某些成员变量本身是指向在主对象的构造函数/析构函数中创建/删除的对象的指针,或者如果它们具有更专业的赋值运算符或其他自定义逻辑,那么您就可以了你手上还有更多的工作,但对于琐碎的成员变量,这应该足够好了。