5

基本クラスのデストラクタの this-pointer に奇妙な問題があります。

問題の説明:

私は3つのクラスを持っています: A1A2A3

A2はA1からパブリックに継承し、 A3からプライベートに継承します

class A2:private A3, public A1 {...}

A3には関数getPrimaryInstance()があります ... A2インスタンスへのA1タイプの参照を返します。

A1& A3::getPrimaryInstance()const{
    static A2 primary;
    return primary;
}

A3コンストラクターは次のようになります。

A3(){
    getPrimaryInstance().regInst(this);
}

( regInst(...)は、すべてのA3インスタンスへのポインターを格納するA1で定義された関数です)

同様にA3デストラクタ:

~A3(){
    getPrimaryInstance().unregInst(this);
}

↑ここで問題発生!

primaryという名前の静的なA2インスタンスがプログラムの終了時に破棄されると、 A3デストラクタが呼び出されますが、~A3内では、破棄するのと同じインスタンスの関数にアクセスしようとします。 =>実行時のアクセス違反!

したがって、次のような単純なifステートメントで修正できると思いました。

~A3(){
    if(this != (A3*)(A2*)&getPrimaryInstance()) //Original verison
    //if (this != dynamic_cast<A3*>(static_cast<A2*>(&getPrimaryInstance())))
    //Not working. Problem with seeing definitions, see comments below this post

        getPrimaryInstance().unregInst(this);
}

(二重キャストの理由は継承です:)
A1 A3
. \ /
. A2
(しかし、それは重要ではありません。単に(int)キャストなどを使用できます)

キッカーは、それがまだクラッシュすることです。デバッガーでコードをステップ実行すると、A2 プライマリインスタンスが破棄されたときに、デストラクタのthisポインターとgetPrimaryInstance()を呼び出して取得したアドレスが何らかの理由でまったく一致しないことがわかります。this -pointer が指すアドレスが、(私の限られた知識では) あるべきアドレスと常に異なる理由を理解できません。:(

デストラクタでこれを行う:

int test = (int)this - (int)&getPrimaryInstance();

また、違いが一定ではないことも示しました(一定のオフセットがあるという理論が簡単にありました)ので、同じものであるはずの2つの完全に異なるオブジェクトのようです。:(

私は VC++ Express (2008) でコーディングしています。少しグーグルで調べた後、次の MS 記事を見つけました:
FIX: The "this" Pointer Is Incorrect in Destructor of Base Class

これは私が抱えている問題とは異なります (また、C++.Net 2003 で修正されたと思われます)。しかし、症状は非常に似ているように見え簡単回避
策が提示されたので、 試しみることにしました.

class A2:private A3, public A1 {...} // <-- old version
class A2:private A3, virtual public A1 {...} //new, with virtual!

そしてそれはうまくいきました!this -pointerはまだ間違っているように見えますが、アクセス違反は発生しなくなりました。

だから私の大きな質問はなぜですか?this
-pointer が本来あるべき場所を指していない のはなぜですか? 上記のように継承にvirtualを 追加すると解決するのはなぜですか(これはまだ&getPrimaryInstance()以外の場所を指しているにもかかわらず)? これはバグですか?MS 以外の環境で試すことはできますか? そして最も重要なこと:これは安全ですか?? 確かにもう文句は言いませんが、私はまだそれがすべきことをしていないのではないかと心配しています. :S


誰かがこれについての知識や経験を持っていて、それを理解するのを手伝ってくれるなら、私はとても感謝しています.

4

8 に答える 8

4

C キャストの使用はあなたを殺しています。
多重継承がある状況では特に壊れやすいです。

クラス階層をキャストダウンするには、dynamic_cast<> を使用する必要があります。static_cast<> を使用して上に移動できますが (私が行ったように)、dynamic_cast<> を使用して両方向に移動する方が明確だと思う場合があります。

C++ コードで C スタイルのキャストを避ける

C++ には、C スタイルのキャストを置き換えるために設計された 4 種類のキャストがあります。あなたは reinterpret_cast<> と同等のものを使用していて、間違って使用しています (reinterpret_cast<> を見た優れた C++ 開発者は、ここでちょっと待ってください)。

~A3(){
    if(this != (A3*)(A2*)&getPrimaryInstance())
        getPrimaryInstance().unregInst(this);
}

Should be:

~A3()
{
   if (this != dynamic_cast<A3*>(static_cast<A2*>(&getPrimaryInstance())))
   {    getPrimaryInstance().unregInst(this);
   }
}

A2 オブジェクトのレイアウト: (おそらくこのようなもの)。

     Offset   Data
A2   0x00   |------------------
     0x10   * A3 Stuff
            *------------------
     0x20   * A1 Stuff
            *------------------
     0x30   * A2 Stuff

getPrimaryInstance() で

 // Lets assume:
 std::cout << primary; // has an address of 0xFF00

返される参照は、オブジェクトの A1 部分を指します。

std::cout << &getPrimaryInstancce();
// Should now print out 0xFF20

C スタイルのキャストを使用する場合、何もチェックせず、型を変更するだけです。

std::cout << (A2*)&getPrimaryInstancce();
// Should now print out 0xFF20
std::cout << (A3*)(A2*)&getPrimaryInstancce();
// Should now print out 0xFF20

ただし、C++ キャストを使用する場合は、正しく補正する必要があります。

std::cout << static_cast<A2*>(&getPrimaryInstance());
// Should now print out 0xFF00
std::cout << dynamic_cast<A3*>(static_cast<A2*>(&getPrimaryInstance()));
// Should now print out 0xFF10

もちろん、実際の値はすべてコンパイラに大きく依存し、実装レイアウトに依存します。上記は、起こり得ることのほんの一例です。

指摘したように、現在破棄中のオブジェクトで dynamic_cast<> を呼び出すのはおそらく安全ではありません。

ではどうですか

最初に登録されたオブジェクトに対して true を返すように regInst() を変更します。getPrimaryInstance() は常に最初に作成されるオブジェクトによって行われるため、常に最初に自分自身を登録します。

この結果をローカル メンバー変数に格納し、最初でない場合にのみ登録を解除します。

A3()
{
    m_IamFirst = getPrimaryInstance().regInst(this);
}

~A3()
{
    if (!m_IamFirst)
    {
         getPrimaryInstance().unregInst(this);
    }
}

質問:

this-pointer が本来あるべき場所を指していないのはなぜですか?

します。C-Cast を使用するだけでポインターがねじ込まれます。

上記のように継承に virtual を追加すると解決するのはなぜですか (これがまだ &getPrimaryInstance() 以外の場所を指しているにもかかわらず)。

C-Castがポインターを台無しにしないように、メモリ内のレイアウトを変更するためです。

これはバグですか?

いいえ、C-Cast の間違った使い方

MS 以外の環境で試すことはできますか?

それは似たようなことをします。

そして最も重要なこと: これは安全ですか??

いいえ。仮想メンバーがどのようにレイアウトされるかは実装定義です。たまたま運がいいだけです。

解決策: C スタイルのキャストの使用を停止します。適切な C++ キャストを使用します。

于 2010-12-07T18:48:40.757 に答える
3
class A2 : private A3, public A1 {...} // <-- old version
class A2 : private A3, virtual public A1 {...} //new, with virtual!  

上記のように継承に virtual を追加すると解決するのはなぜですか (これがまだ &getPrimaryInstance() 以外の場所を指しているにもかかわらず)。

これが違いを生む理由は、virtual継承が基本クラスのコンストラクターと基本クラスのデストラクタが呼び出される順序に影響するためです。

ポインタの数値thisが同じでない理由は、完全なオブジェクトの異なる「基本クラスのサブオブジェクト」A2 primary;が異なるアドレスを持つことができ、また異なるアドレスを持つ必要があるためです。デストラクタが呼び出される前に、 を使用dynamic_castして と の間A1*を移動できますA2*。オブジェクトが実際に のプライベート ベース部分であることが確実な場合は、C スタイルのキャストを使用して から を取得できます。A3A2A3*A2*

しかし、デストラクタの本体~A2が完了すると (デストラクタ内の場合~A3)、dynamic_castfrom A1*toA2*は失敗し、C スタイルのキャスト from A3*toは未定義の動作を生成します。これは、オブジェクトA2*がなくなるためです。A2

A2 primary;そのため、保存/アクセス方法を変更しない限り、おそらくあなたがしようとしていることを行う方法はありません.

于 2010-12-07T16:26:30.763 に答える
2

The virtual base class should only come into play if you have a diamond-inheritance structure, i.e. you're multiply inheriting classes sharing a common base class. Are you showing the whole actual inheritance tree of A1, A2 and A3 in your actual code?

于 2010-12-07T15:31:14.730 に答える
1

問題は、A2 オブジェクトに対して A3::~A3() が呼び出されると、A2 の破棄の最後に ~A3() が呼び出されるため、A2 オブジェクトが既に破棄されていることです。getPrimary は既に破棄されているため、再度呼び出すことはできません。注意: これは静的変数 primary の場合に適用されます

于 2010-12-07T15:18:24.453 に答える
1

OP @AnorZaken は次のようにコメントしています。

...これは私が取り組もうとした当初の問題の 1 つでした。getPrimaryInstance() で A2 参照を直接返したいのですが、できません! A3 は A2 の定義を見ていません! getPrimaryInstance() は A3 の基本クラスで宣言されているため (上記には記載されていません)、エラー C2555: 'A3::getPrimaryInstance': 仮想関数の戻り値の型が異なり、'A3Base::getPrimaryInstance' と共変ではありません。 A2 の存在を宣言したとしても、A2 を宣言する前に、A2 がベースとして A1 を持っていることをコンパイラーに伝える方法がわかりません。:(これを解決できれば最高です!

したがって、次のようなものがあるようです。

class A3Base {
public:
  virtual A1& getPrimaryInstance();
};

そして はのclass A2前に定義できないためclass A3、共変の戻り値の型をスキップします。A2&から参照を取得する方法が必要な場合は、それをのメソッドA3として追加します。

// A3.hpp
class A2;

class A3 : public A3Base {
public:
  virtual A1& getPrimaryInstance();
  A2& getPrimaryInstanceAsA2();
};

// A3.cpp
#include "A3.hpp"
#include "A2.hpp"

A1& A3::getPrimaryInstance() {
    return getPrimaryInstanceAsA2(); // no cast needed for "upward" public conversion
}
于 2010-12-07T20:50:51.973 に答える
0

プログラムの終了時にprimaryという名前の静的A2インスタンスが破棄されると、A3デストラクタが呼び出されますが、〜A3内では、破棄するのと同じインスタンスの関数にアクセスしようとします。=>実行時のアクセス違反!

primaryという名前の静的A2インスタンスが破棄されると、 primaryへのポインタはメモリ内の「ランダムな」場所を指します。したがって、ランダムなメモリ位置にアクセスしようとすると、ランタイム違反が発生します。それはすべて、デストラクタを呼び出し、デストラクタで呼び出す順序と関係があります。

次のようなものを試してください:

delete a3;
delete a2-primary;

それ以外の

delete a2-primary;
delete a3;

また、この型キャストチュートリアルが役立つと思うかもしれません。

私があなたを助けることができることを願っています。

于 2010-12-07T16:02:00.230 に答える
0

あなたが尋ねるべき2つのはるかに大きな質問があります:

  1. なぜプライベート継承を使用する必要があるのですか?
  2. 非抽象基本クラスに多重継承を使用する必要があるのはなぜですか?

ほとんどの場合、#1の答えはあなたがしないことです。データメンバーを含むクラスを他のクラスに宣言すると、通常、この同じ状況がはるかにクリーンで保守しやすいコードベースで処理されます。

ほとんどの場合、#2の答えはあなたもそうではないということです。それはあなたがあなた自身の危険で使う言語の特徴です。

Meyersの本を読んで、デザインパターンを再評価することをお勧めします。

于 2010-12-07T16:03:34.490 に答える
-1

簡潔な答え。間違ったデストラクタが呼び出されているために発生します。長い回答については、これこれを確認して ください。そして、スコット・マイヤーの効果的な C++ を確認してください。そんな疑問をカバーしています。

于 2010-12-07T15:20:47.307 に答える