web-dev-qa-db-ja.com

インスタンス化後にC ++オブジェクトのクラスを変更することは可能ですか?

共通の基本クラスから同じ属性をすべて継承するクラスがたくさんあります。基本クラスは一般的なケースで機能する仮想関数を実装しますが、各サブクラスはさまざまな特殊なケースのためにそれらの仮想関数を再実装します。

状況は次のとおりです。これらのサブクラス化されたオブジェクトの特殊性を消耗品にしたいのです。基本的に、オブジェクトがそのサブクラスのアイデンティティを失い、基本クラスで実装された一般的なケースの振る舞いを持つ基本クラスのインスタンスに戻るようにする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;
}
41
dwk

良い習慣を破り、安全でないコードを維持するという犠牲を払うことができます。他の答えは、これを達成するための厄介なトリックを提供します。

「やるべきではない」という答えは好きではありませんが、おそらくあなたが求める結果を達成するためのより良い方法があることを提案したいと思います。

@ manni66のコメントで示唆されている strategy pattern は良いものです。

また、 データ指向設計 についても考慮する必要があります。クラス階層は、あなたの場合には賢明な選択のようには見えないからです。

35

はいといいえ。 C++クラスは、オブジェクトであるメモリ領域のタイプを定義します。メモリ領域がインスタンス化されると、そのタイプが設定されます。 around型システムは確かに動作するように試みることができますが、コンパイラはそれで逃げさせません。遅かれ早かれ、コンパイラーは違反した型についての仮定を行ったので、足を踏み入れるでしょう。コンパイラーが移植可能な方法でそのような仮定を行うのを止める方法はありません。

ただし、これには設計パターンがあります。「状態」です。独自の基本クラスを使用して、変更内容を独自のクラス階層に抽出し、この新しい階層の抽象状態ベースへのポインターをオブジェクトに保存します。その後、それらをあなたの心のコンテンツに交換できます。

16
StoryTeller

いいえ、一度インスタンス化されたオブジェクトのタイプを変更することはできません。

*object = baseObject;objecttypeを変更せず、単にコンパイラーが生成した代入演算子を呼び出します。

あなたが書いていた場合、それは別の問題だったでしょう

object = new Base;

deleteを自然に呼び出すことを忘れないでください;現在、あなたのコードはオブジェクトをリークしています)。

C++ 11以降では、resourcesをあるオブジェクトから別のオブジェクトに移動できます。見る

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

14
Bathsheba

同じメモリアドレスで新しいオブジェクトを作成できる限り、古いオブジェクトを破棄して新しいオブジェクトを作成することができます。そのため、既存のポインターは壊れません。

C++標準は、セクション3.8(オブジェクトの有効期間)でこの考えに明示的に対処しています。

If、オブジェクトのライフタイムが終了した後、オブジェクトが占有していたストレージが再利用または解放される前元のオブジェクトが占有していた保存場所に新しいオブジェクトが作成されます。元のオブジェクトを指すポインター、元のオブジェクトを参照した参照、または元のオブジェクトの名前は新しいオブジェクトを自動的に参照し、新しいオブジェクトのライフタイムが開始されると、を使用して新しいオブジェクトを操作できます<snip>

ああ、これはまさにあなたが望んでいたことです。しかし、ルール全体を示したわけではありません。残りは次のとおりです。

if

  • 新しいオブジェクトのストレージは、元のオブジェクトが占有していたストレージの場所を正確にオーバーレイします。
  • 新しいオブジェクトは元のオブジェクトと同じタイプです(トップレベルのcv-qualifiersを無視します)
  • 元のオブジェクトの型はconst修飾されておらず、クラス型の場合、その型がconst修飾された非静的データメンバーまたは参照型を含んでいない。
  • 元のオブジェクトはタイプTの最も派生したオブジェクト(1.8)であり、新しいオブジェクトはタイプTの最も派生したオブジェクトです(つまり、これらは基本クラスのサブオブジェクトではありません)。

あなたの考えは言語委員会によって考えられ、特に違法にされました。「正しいタイプの基本クラスサブオブジェクトがあるので、その場所に新しいオブジェクトを作成するだけです」という最後の弾丸そのトラックで停止します。

@RossRidgeの答えが示すように、オブジェクトを別のタイプのオブジェクトに置き換えることができます。または、オブジェクトを置換し、置換前に存在していたポインターを使用し続けることができます。ただし、両方を同時に行うことはできません。

ただし、 有名な引用:「間接参照のレイヤーを追加することでコンピューターサイエンスの問題を解決できます」 のように、ここでも同じことが言えます。

提案された方法の代わりに

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などの「デザインパターン」が得られます。しかし、それらはすべて1つの追加レベルの間接参照に依存しています。

13
Ben Voigt

新しい配置と明示的なデストラクタ呼び出しを使用して、文字通り求めていることを実行できます。このようなもの:

#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が言ったように、これは本当に良いデザインではありません。あなたがやろうとしていることを再考することをお勧めします。ここの他の回答のいくつかもあなたの問題を解決するかもしれませんが、私はあなたが求めているもののアイデアに沿ったものは何でも手がかりになると思います。オブジェクトのタイプが変わったときにポインターを変更できるように、アプリケーションの設計を真剣に検討する必要があります。

9
Ross Ridge

戦略パターンを使用することをお勧めします。

#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)のユニットテストは簡単です
  • アナウンサーはコード内の別の場所で再利用できます

古くからある「構成と継承」の議論をご覧になることをお勧めします(cf. https://www.thoughtworks.com/insights/blog/composition-vs-inheritance-how-choose =)

追伸元の投稿でDerivedをリークしました!可能な場合は、std :: unique_ptrをご覧ください。

9
D3C34C34D

基本クラスに変数を導入することにより、メモリフットプリントを同じに保つことができます。フラグを設定すると、派生クラスまたは基本クラスの実装を強制的に呼び出すことができます。

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

他の答えに加えて、関数ポインター(または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;
}

また、このような関数ポインターアプローチを使用すると、派生クラスに実装されている既に定義済みのメンバーセットに限定されるのではなく、実行時にオブジェクトの関数を変更できます。

7
alexeykuzmin0

タイプを正規化することを検討します。

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スマートポインタのインスタンスですが、気にしません。

プログラムに単純なエラーがあります。オブジェクトを割り当てますが、ポインターは割り当てません。

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

    Base baseObject;

baseObjectDerivedオブジェクトで上書きする*objectBaseを割り当てます。ただし、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型の2つのオブジェクトしかありませんが、もっと動的な動作が必要だと思います。 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;
}

さまざまなユースケースを実装する戦略のような代替パターンがあります。さまざまな問題を解決します。意図と動機のセクションに特に焦点を当てて、パターンのドキュメントを読むことをお勧めします。

3
Jens

これは素晴らしいデザインではないというアドバイスには同意しませんが、別の安全な方法は、切り替えたいクラスのいずれかを保持できるユニオンを使用することです。そのうちの。以下は、ユニオン自体の内部にすべての詳細をカプセル化したバージョンです。

#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を宣言することを忘れないでください!多態性クラスではこれが重要です。したがって、ベースクラスのデストラクタではなく、そのインスタンスに適切なデストラクタを呼び出します。)クラス。ただし、安全な自動削除を行うには、スマートポインターを操作する必要があります。動的メモリへのスマートポインタに対するユニオンの利点の1つは、ヒープ上のオブジェクトをこれ以上割り当てたり解放したりする必要はなく、所有しているメモリを再利用できることです。

1
Davislor

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
}

これはもちろん絶対的な裸骨です。移動ctor、間接参照演算子などを追加できます。

0
bolov

免責事項:ここのコードは、実稼働環境では実装しないで、アイデアを理解するための手段として提供されています。

継承を使用しています。 3つのことを達成できます。

  • フィールドを追加
  • メソッドを追加する
  • 仮想メソッドを置き換える

これらすべての機能のうち、最後のものだけを使用しています。つまり、実際には継承に依存する必要はありません。他の多くの方法で同じ結果を得ることができます。最も簡単なのは、自分で「タイプ」のタブを保持することです。これにより、オンザフライでタブを変更できます。

#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を介して実装する外部クラスに置き換えることができます!他の人が最初に言ったように、私たちは戦略パターンを再発明しました:)

肝心なのは、何をするにしても、メインクラスを継承していないということです。

0
Agent_L

インスタンス化後にオブジェクトのタイプに変更することはできません。この例でわかるように、ベースクラス(ベースクラスのタイプ)へのポインタがあるため、このタイプは最後までスタックします。

  • ベースポインターが上位オブジェクトまたは下位オブジェクトを指すことは、そのタイプの変更を意味するものではありません。

    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;

予想通りではない結果。

0
Raindrop7