2012-04-24 27 views
1

(C++、MinGWの4.4.0、WindowsのOS)C++:バーチャルファンクションは "this"ポインタスコープの問題をどのように解決しますか?

すべてのことのラベル< 1>と< 2>を除き、コードにコメントしているが、私の推測です。あなたは、私がどこか間違っていると思う場合には、私を修正してください:

class A { 
public: 
    virtual void disp(); //not necessary to define as placeholder in vtable entry will be 
         //overwritten when derived class's vtable entry is prepared after 
         //invoking Base ctor (unless we do new A instead of new B in main() below) 
}; 

class B :public A { 
public: 
    B() : x(100) {} 
    void disp() {std::printf("%d",x);} 
    int x; 
}; 

int main() { 
    A* aptr=new B;    //memory model and vtable of B (say vtbl_B) is assigned to aptr 
    aptr->disp();    //<1> no error 
    std::printf("%d",aptr->x); //<2> error -> A knows nothing about x 
} 

< 2>はエラーであり、明白です。なぜ< 1>は間違いがありますか?私はこの呼び出しで起こっていると思います:aptr->disp(); --> (*aptr->*(vtbl_B + offset to disp))(aptr)aptrは、暗黙的にメンバー関数へのポインタthisです。内部disp()std::printf("%d",x); --> std::printf("%d",aptr->x); SAME AS std::printf("%d",this->x); < 2>はなぜ< 1>エラーを出さないのですか?

(私は特定とスタッフのvtableが実装されている知っているが、私はまだそれが質問をする価値があると思います)

答えて

3

ルールがある:C++動的ディスパッチで

のみメンバ関数の機能のためではない作品メンバー変数。コンパイラは、その特定のクラスまたはその基底クラス内のシンボル名にlooksupメンバ変数の

。ケース1では、呼び出される適切な方法はvptをフェッチによって決定される

適切な方法のアドレスをフェッチした後、appropiateメンバ関数を呼び出します。
したがって、静的バインディングの場合、通常のcallではなく、動的ディスパッチは基本的にfetch-fetch-callです。

ケース2:コンパイラがthisの範囲内のxのみを検索します。明らかに、見つからず、エラーを報告します。

+0

okですが、 'this'はスライスされて、<1>の' A'だけが分かります。メンバ関数の中では、呼び出しは 'this-> whatEver'に解決されると思います。表示されているように暗黙のうちに渡された「this」は、Bオブジェクトの場合と同じではないので、私は混乱しています。 – ustulation

+0

@ustulation:いいえ、 'this'は' B'型で、メソッド 'B :: disp()'は呼び出されたメソッドです。当然それ自身のメンバーである 'x'にアクセスできます。ここにスライスはありません。 –

+1

実際のルールは_In C++です。動的ディスパッチは仮想メンバーにのみ使用されます_。あなたのルールはこれから、そしてその言語が変数を仮想として定義することを許さないという事実に従います。 –

0
virtual void disp(); //not necessary to define as placeholder in vtable entry will be 
        //overwritten when derived class's vtable entry is prepared after 
        //invoking Base ctor (unless we do new A instead of new B in main() below) 

ご意見は厳密には正しくありません。仮想関数はodr-であり、純粋なものでなければ(逆は必ずしも成り立たない)、それは定義を与えなければならないということを意味する。定義を提供したくない場合は、それを純粋な仮想関数にする必要があります。

あなたがaptr->disp();作品これらの変更のいずれかを作り、disp()派生クラスを呼び出した場合、派生クラスでdisp()ためオーバーライド基底クラスの関数。基底クラス関数は、基底へのポインタを介して呼び出すときには、まだ存在していなければなりません。 xは基本クラスのメンバーではないため、aptr->xは有効な式ではありません。

+0

'は、その定義を提供する必要があることを意味します。定義を提供したくない場合は、それを純粋な仮想関数にする必要があります.'pls try it out。これはA :: disp()の定義なしで動作します。 – ustulation

+0

@ustulation:あなたのために働くという理由だけで、コードが正しいとは限りません。動作するように見えるのは、_undefinedビヘイビアを持つコードの可能な動作の1つです。 –

+0

チャールズ・ベイリーが言っているように、それは未定義の行動です。それが動作するかどうかは、コンパイラ、おそらくベースクラスのコンストラクタとデストラクタが行うことと、それらが可視かどうかによって異なります。 –

1

thisaptrと同じではありません。B::dispです。 B::dispの実装ではBという他の方法と同様にB*としてthisとなります。A*ポインター経由で仮想メソッドを呼び出すと、最初にB*に変換されます(呼び出し中にaptrと必ずしも等しくないように、値が変更される可能性もあります)。

I.e.本当に起こるかは、この記事http://blogs.msdn.com/b/oldnewthing/archive/2004/02/06/68695.aspxをチェックし、より複雑な例について

typedef void (A::*disp_fn_t)(); 
disp_fn_t methodPtr = aptr->vtable[index_of_disp]; // methodPtr == &B::disp 

B* b = static_cast<B*>(aptr); 
(b->*methodPtr)(); // same as b->disp() 

のようなものです。ここで、同じB::dispを呼び出すことができるAの基数が複数ある場合、MSVCは、異なるオフセットでA*ポインタをそれぞれシフトする異なるエントリポイントを生成します。これはもちろん実装固有のものです。他のコンパイラは、例えばvtableのどこかにオフセットを格納することを選択することができます。

+0

この答えは、私の疑いを明確にしてくれます(A *からB *への変換、まだ私が見ていないうちに、自分の知識が不足していて、 'this-> x'が' void B: :disp(B * const this){} 'A *を' this''として渡すとき)..コンパイラはコンパイル時に 'methodPtr ==&B :: disp'か' methodPtr ==&A :: disp'..foo(something)f(&bObj); else f(&aObj); ')の場合は、空のf(A * aptr){aptr-> dispあなたが示したようにstaic_castingのポイント(私は気が狂っています:)).... – ustulation

+0

さらにAとBのメモリモデルが一致し、ここで説明しやすくなります。 B :: disp()は、一番左のBaseクラスの非プライベートメンバーにまだアクセスすることができます。 'this'は' new B'が出会ったときにメンバー変数を見つける/静かに解決すると思います'this-> aMemberVar'は実行時にそれ以上触れられません。 – ustulation

+0

@ustulation最新の回答を確認してください。たとえば、MSVCは、 'B :: disp'を呼び出して、' this :: 'を適切にキャスティングして' B :: disp'自体にジャンプする、ベースへのポインタを通して '' t :: ''を呼び出す可能性のあるすべての方法について "トランポリン"を生成します。 – hamstergene

1

あなたは混乱しており、あなたはよりダイナミックな言語から来ているようです。

C++では、コンパイルとランタイムが明確に分離されています。プログラムを最初にコンパイルしてから実行する必要があります(これらの手順のいずれかが失敗する可能性があります)。


だから、後方に行く:コンパイル約静的情報であるため、

<2>

は、コンパイル時に失敗しました。 aptrはタイプ A*であるため、 Aのすべてのメソッドと属性にこのポインタからアクセスできます。 disp()を宣言したが、 xを宣言していないので、 disp()の呼び出しはコンパイルされますが、 xはありません。

したがって、<2>の失敗はほぼセマンティクスであり、それらはC++標準で定義されています。 <1>への行き方


disp()宣言Aでありますので、それが動作します。これにより、関数の存在が保証されます(Aに定義されていないため、実際にここにいると言います)。

実行時には、C++標準によって意味的に定義されますが、標準では実装ガイダンスは提供されません。ほとんどの(すべてではないにしろ)C++コンパイラは、インスタンス戦略ごとにクラス+仮想ポインタごとに仮想テーブルを使用します。この場合、あなたの記述は正しいように見えます。

しかし、これは純粋なランタイム実装であり、プログラムがコンパイルされたという事実には遡及的には影響しません。

関連する問題