12

C ++では、コンストラクター内から仮想関数が呼び出されると、仮想関数のようには動作しません。

この振る舞いに初めて遭遇した人は誰もが驚いたと思いますが、考え直してみると、それは理にかなっています。

派生コンストラクターが実行されていない限り、オブジェクトはまだ派生インスタンスではありません。

では、派生関数をどのように呼び出すことができますか?前提条件を設定する機会がありませんでした。例:

class base {
public:
    base()
    {
        std::cout << "foo is " << foo() << std::endl;
    }
    virtual int foo() { return 42; }
};

class derived : public base {
    int* ptr_;
public:
    derived(int i) : ptr_(new int(i*i)) { }
    // The following cannot be called before derived::derived due to how C++ behaves, 
    // if it was possible... Kaboom!
    virtual int foo()   { return *ptr_; } 
};

Javaと.NETでもまったく同じですが、反対の方向に進むことを選択しました。おそらく、驚き最小の原則の唯一の理由は何でしょうか。

どちらが正しい選択だと思いますか?

4

6 に答える 6

11

言語がオブジェクトの寿命を定義する方法には、根本的な違いがあります。Java および .Net では、コンストラクターが実行される前にオブジェクト メンバーがゼロ/null で初期化され、この時点でオブジェクトの有効期間が開始されます。したがって、コンストラクターに入ると、既に初期化されたオブジェクトが取得されています。

C++ では、オブジェクトの有効期間はコンストラクターが終了したときにのみ開始されます (ただし、メンバー変数と基底クラスは開始前に完全に構​​築されます)。これは、仮想関数が呼び出されたときの動作と、コンストラクターの本体に例外がある場合にデストラクタが実行されない理由を説明しています。

オブジェクトの有効期間の Java/.Net 定義の問題は、オブジェクトが初期化されたがコンストラクターが実行されていない場合の特別なケースを設定することなく、オブジェクトが常にその不変条件を満たしていることを確認するのが難しいことです。C++ の定義の問題は、オブジェクトが辺鄙な状態にあり、完全に構築されていない奇妙な期間があることです。

于 2008-09-16T18:49:55.313 に答える
7

どちらの方法でも、予期しない結果が生じる可能性があります。最善の策は、コンストラクターで仮想関数をまったく呼び出さないことです。

私が思うC++の方法はもっと理にかなっていますが、誰かがあなたのコードをレビューするときに期待の問題を引き起こします。この状況に気付いた場合は、後でデバッグするために、意図的にコードをこの状況に置かないでください。

于 2008-08-31T12:47:34.727 に答える
2

コンストラクターの仮想関数、言語が異なるのはなぜですか?

誰も良い振る舞いをしないからです。C++ の動作の方が理にかなっていることがわかります (基本クラスの c-tor が最初に呼び出されるため、基本クラスの仮想関数を呼び出す必要があるのは当然のことです。結局のところ、派生クラスの c-tor はまだ実行されていないため、派生クラスの仮想関数に適切な前提条件が設定されていない可能性があります)。

しかし、仮想関数を使用して状態を初期化したい場合 (したがって、仮想関数が初期化されていない状態で呼び出されても問題ありません)、C#/Java の動作の方が優れています。

于 2008-09-01T00:57:41.650 に答える
1

C ++は、「最も正しい」動作をするという点で最高のセマンティクスを提供すると思います...ただし、コンパイラーにとってはより多くの作業が必要であり、コードは後で読む人にとっては間違いなく直感的ではありません。

.NETアプローチでは、派生オブジェクトの状態に依存しないように、関数を非常に制限する必要があります。

于 2008-08-31T12:56:28.340 に答える
0

私はC++の振る舞いが非常に煩わしいことに気づきました。たとえば、オブジェクトの目的のサイズを返し、デフォルトのコンストラクターで各アイテムを初期化する仮想関数を作成することはできません。たとえば、次のようにすると便利です。

BaseClass() { for (int i=0; i<virtualSize(); i++) initialize_stuff_for_index(i); }

また、C ++の動作の利点は、上記のようなコンストラクターが記述されないようにすることです。

コンストラクターが終了したと仮定してメソッドを呼び出す問題は、C++の良い言い訳にはならないと思います。これが本当に問題である場合、同じ問題が基本クラスのメソッドに適用される可能性があるため、コンストラクターはメソッドを呼び出すことができません。

C ++に対するもう1つのポイントは、動作の効率がはるかに低いことです。コンストラクターはそれが何を呼び出すかを直接知っていますが、コンストラクターは仮想関数を呼び出す他のメソッドを呼び出す可能性があるため、ベースからファイナルまですべてのクラスに対してvtabポインターを変更する必要があります。私の経験から、これはコンストラクターで仮想関数呼び出しをより効率的にすることによって節約されるよりもはるかに多くの時間を浪費します。

はるかに厄介なのは、これがデストラクタにも当てはまるということです。仮想cleanup()関数を記述し、基本クラスのデストラクタがcleanup()を実行する場合、それは確かに期待どおりに実行されません。

これと、C ++が終了時に静的オブジェクトのデストラクタを呼び出すという事実は、長い間私を本当に怒らせてきました。

于 2008-09-16T21:41:24.090 に答える
0

Delphi は、VCL GUI フレームワークで仮想コンストラクタをうまく利用しています。

type
  TComponent = class
  public
    constructor Create(AOwner: TComponent); virtual; // virtual constructor
  end;

  TMyEdit = class(TComponent)
  public
    constructor Create(AOwner: TComponent); override; // override virtual constructor
  end;

  TMyButton = class(TComponent)
  public
    constructor Create(AOwner: TComponent); override; // override virtual constructor
  end;

  TComponentClass = class of TComponent;

function CreateAComponent(ComponentClass: TComponentClass; AOwner: TComponent): TComponent;
begin
  Result := ComponentClass.Create(AOwner);
end;

var
  MyEdit: TMyEdit;
  MyButton: TMyButton;
begin
  MyEdit := CreateAComponent(TMyEdit, Form) as TMyEdit;
  MyButton := CreateAComponent(TMyButton, Form) as TMyButton;
end;
于 2008-09-16T17:50:12.210 に答える