32

子クラスが有効な状態にない可能性があるため、基本クラスのコンストラクターから仮想メソッドを呼び出すことは危険な場合があることを知っています。(少なくとも C# では)

私の質問は、仮想メソッドがオブジェクトの状態を初期化するものである場合はどうなりますか? 最初にオブジェクトを作成してから状態をロードするのは良い習慣ですか、それとも2段階のプロセスであるべきですか?

最初のオプション: (コンストラクターを使用して状態を初期化する)

public class BaseObject {
    public BaseObject(XElement definition) {
        this.LoadState(definition);
    }

    protected abstract LoadState(XElement definition);
}

2 番目のオプション: (2 段階のプロセスを使用)

public class BaseObject {
    public void LoadState(XElement definition) {
        this.LoadStateCore(definition);
    }

    protected abstract LoadStateCore(XElement definition);
}

最初のメソッドでは、コードのコンシューマは 1 つのステートメントでオブジェクトを作成および初期化できます。

// The base class will call the virtual method to load the state.
ChildObject o = new ChildObject(definition)

2 番目の方法では、コンシューマーはオブジェクトを作成してから状態をロードする必要があります。

ChildObject o = new ChildObject();
o.LoadState(definition);
4

9 に答える 9

39

(この回答はC#とJavaに適用されます。C++はこの問題で異なる動作をすると思います。)

コンストラクターで仮想メソッドを呼び出すことは確かに危険ですが、場合によっては最もクリーンなコードになる可能性があります。

私は可能な限りそれを避けようとしますが、デザインを大きく曲げることはありませ。(たとえば、「後で初期化」オプションは不変性を禁止します。)コンストラクターで仮想メソッドを使用する場合は、それを非常に強力に文書化します。関係者全員がそれが何をしているのかを知っている限り、それがあまり多くの問題を引き起こすことはないはずです。ただし、最初の例で行ったように、可視性を制限しようとします。

編集:ここで重要なことの1つは、初期化の順序でC#とJavaの間に違いがあるということです。次のようなクラスがある場合:

public class Child : Parent
{
    private int foo = 10;

    protected override void ShowFoo()
    {
        Console.WriteLine(foo);
    }
}

コンストラクターがParentを呼び出すShowFoo場合、C#では10が表示されます。Javaの同等のプログラムは0を表示します。

于 2009-01-15T20:10:39.267 に答える
10

C ++では、基本クラスコンストラクターで仮想メソッドを呼び出すと、派生クラスがまだ存在しないかのようにメソッドが呼び出されます(存在しないため)。つまり、呼び出しはコンパイル時に、基本クラス(またはそれが派生したクラス)で呼び出す必要のあるメソッドに解決されます。

GCCでテストすると、コンストラクターから純粋仮想関数を呼び出すことができますが、警告が表示され、リンク時エラーが発生します。この動作は標準では定義されていないようです。

「メンバー関数は、抽象クラスのコンストラクタ(またはデストラクタ)から呼び出すことができます。そのようなオブジェクトから直接または間接的に作成(または破棄)されるオブジェクトに対して、純粋仮想関数(class.virtual)を仮想呼び出し( class.virtual )する効果。コンストラクタ(またはデストラクタ)は未定義です。」

于 2009-01-15T20:18:50.653 に答える
4

C ++では、仮想メソッドは、構築されているクラスのvtableを介してルーティングされます。したがって、この例では、BaseObjectの構築中に、呼び出すLoadStateCoreメソッドがないため、純粋仮想メソッドの例外が生成されます。

関数が抽象的ではなく、単に何もしない場合、関数が実際に呼び出されない理由を思い出そうとして、プログラマーがしばらく頭を悩ませることがよくあります。

このため、C++ではこのように行うことはできません...

于 2009-01-15T20:19:27.067 に答える
4

C++ の場合、基本コンストラクターは派生コンストラクターの前に呼び出されます。これは、仮想テーブル (派生クラスのオーバーライドされた仮想関数のアドレスを保持する) がまだ存在しないことを意味します。このため、これを行うのは非常に危険であると考えられています (特に、関数が基本クラスで純粋仮想である場合...これにより、純粋仮想例外が発生します)。

これには 2 つの方法があります。

  1. 構築+初期化の2段階のプロセスを実行します
  2. 仮想関数を、より厳密に制御できる内部クラスに移動します (上記のアプローチを利用できます。詳細については、例を参照してください)。

(1) の例は次のとおりです。

class base
{
public:
    base()
    {
      // only initialize base's members
    }

    virtual ~base()
    {
      // only release base's members
    }

    virtual bool initialize(/* whatever goes here */) = 0;
};

class derived : public base
{
public:
    derived ()
    {
      // only initialize derived 's members
    }

    virtual ~derived ()
    {
      // only release derived 's members
    }

    virtual bool initialize(/* whatever goes here */)
    {
      // do your further initialization here
      // return success/failure
    }
};

(2) の例は次のとおりです。

class accessible
{
private:
    class accessible_impl
    {
    protected:
        accessible_impl()
        {
          // only initialize accessible_impl's members
        }

    public:
        static accessible_impl* create_impl(/* params for this factory func */);

        virtual ~accessible_impl()
        {
          // only release accessible_impl's members
        }

        virtual bool initialize(/* whatever goes here */) = 0;
    };

    accessible_impl* m_impl;

public:
    accessible()
    {
        m_impl = accessible_impl::create_impl(/* params to determine the exact type needed */);

        if (m_impl)
        {
            m_impl->initialize(/* ... */);  // add any initialization checking you need
        }
    }

    virtual ~accessible()
    {
        if (m_impl)
        {
            delete m_impl;
        }
    }

    /* Other functionality of accessible, which may or may not use the impl class */
};

アプローチ (2) では、Factory パターンを使用して、クラスに適切な実装を提供しaccessibleます (クラスと同じインターフェイスを提供しますbase)。ここでの主な利点の 1 つは、の仮想メンバーを安全にaccessible利用できる の構築中に初期化できることです。accessible_impl

于 2009-01-15T22:33:28.853 に答える
3

投稿に示されているようにXElement、コンストラクターを使用XElementするクラスがある場合、派生クラスのみが由来する可能性があります。では、すでに。を持っている派生クラスに状態をロードしてみませんかXElement

あなたの例には状況を変えるいくつかの基本的な情報が欠けているか、または基本クラスからの情報で派生クラスに戻る必要はありません。正確な情報を教えてくれたからです。

すなわち

public class BaseClass
{
    public BaseClass(XElement defintion)
    {
        // base class loads state here
    }
}

public class DerivedClass : BaseClass
{
    public DerivedClass (XElement defintion)
        : base(definition)
    {
        // derived class loads state here
    }
}

そうすれば、コードは本当に単純で、仮想メソッド呼び出しの問題は発生しません。

于 2009-01-15T22:48:53.957 に答える
3

C ++の場合、標準のセクション12.7、パラグラフ3がこのケースをカバーしています。

要約すると、これは合法です。実行中のコンストラクターのタイプに合った正しい関数に解決されます。したがって、例をC ++構文に適合させると、を呼び出すことになりますBaseObject::LoadState()。に到達することはできませんChildObject::LoadState()。クラスと関数を指定してアクセスしようとすると、未定義の動作が発生します。

抽象クラスのコンストラクターについては、セクション10.4の段落6で説明しています。簡単に言うと、メンバー関数を呼び出すことはできますが、コンストラクターで純粋仮想関数を呼び出すことは未定義の動作です。そうしないでください。

于 2009-01-15T20:32:38.240 に答える
3

C++ については、Scott Meyer の対応する記事を参照してください。

構築中または破棄中に仮想関数を呼び出さない

ps: 記事のこの例外に注意してください:

logTransaction 関数はトランザクション内で純粋に仮想的であるため、問題はほぼ確実に実行前に明らかになります。定義されていない限り (可能性は低いですが )、プログラムはリンクしません。リンカは必要な Transaction::logTransaction の実装を見つけることができません。

于 2009-01-16T10:51:45.290 に答える
1

C ++では、いくつかの制限がありますが、基本クラス内から仮想関数を呼び出すことは完全に安全です(純粋でない限り) 。しかし、あなたはそれをすべきではありません。コメントと適切な名前(のような)を使用してそのような初期化関数として明示的にマークされている非仮想関数を使用して、オブジェクトをより適切に初期化しますinitialize。それを呼び出すクラスで純粋仮想として宣言されている場合でも、動作は定義されていません。

呼び出されるバージョンは、コンストラクター内から呼び出すクラスの1つであり、派生クラスのオーバーライドではありません。これは仮想関数テーブルとはあまり関係がありませんが、その関数のオーバーライドがまだ初期化されていないクラスに属している可能性があるという事実があります。したがって、これは禁止されています。

C#とJavaでは、コンストラクターの本体に入る直前に行われるデフォルトの初期化などがないため、これは問題ではありません。C#では、本体の外部で行われるのは、基本クラスまたは兄弟コンストラクターを呼び出すことだけです。ただし、C ++では、派生クラスのコンストラクター本体に入る直前にコンストラクター初期化子リストを処理しているときに、その関数のオーバーライドによって派生クラスのメンバーに対して行われた初期化は、それらのメンバーを構築するときに取り消されます。

編集:コメントのため、少し説明が必要だと思います。これが(考案された)例です。仮想の呼び出しが許可され、呼び出しによって最終的なオーバーライドがアクティブになると仮定します。

struct base {
    base() { init(); }
    virtual void init() = 0;
};

struct derived : base {
    derived() {
        // we would expect str to be "called it", but actually the
        // default constructor of it initialized it to an empty string
    }
    virtual void init() {
        // note, str not yet constructed, but we can't know this, because
        // we could have called from derived's constructors body too
        str = "called it";
    }
private:
    string str;
};

この問題は、C ++標準を変更して許可することで実際に解決できます。コンストラクターの定義、オブジェクトの有効期間などを調整します。str = ...;まだ構築されていないオブジェクトの意味を定義するためのルールを作成する必要があります。そして、その効果が誰に電話をかけたかによってどのように異なるかに注意してくださいinit。私たちが得た機能は、そのときに解決しなければならない問題を正当化するものではありません。したがって、C ++は、オブジェクトの構築中に動的ディスパッチを禁止するだけです。

于 2009-01-15T22:53:14.603 に答える
1

通常、これらの問題は、貪欲なベースコンストラクターを使用することで回避できます。この例では、XElementをLoadStateに渡しています。基本コンストラクターで状態を直接設定できるようにすると、子クラスはコンストラクターを呼び出す前にXElementを解析できます。

public abstract class BaseObject {
   public BaseObject(int state1, string state2, /* blah, blah */) {
      this.State1 = state1;
      this.State2 = state2;
      /* blah, blah */
   }
}

public class ChildObject : BaseObject {
   public ChildObject(XElement definition) : 
      base(int.Parse(definition["state1"]), definition["state2"], /* blah, blah */) {
   }
}

子クラスがかなりの作業を行う必要がある場合は、静的メソッドにオフロードできます。

于 2009-01-15T20:33:50.017 に答える