1

私は、かなり大きなプロジェクトを複数のインターフェースと実装で持っています。 コードはg ++を使ってLinux環境で実装されています(5.4と思います)。コードをWindowsに移植し、VS15(MSVC v140)で実行した後、キャストされたポインタにアクセスしようとした後にアクセス違反が発生しました。msvcのC++アクセス違反ですが、多重継承とキャストのためのgccではありません

これは以下のコードで継承階層である:実際のコードで

    A  
       /\ 
    virtual / \ 
      / | 
      B  | 
      |  | virtual 
      C  | 
      |  | 
      \ /
       \ /
       D 

継承のデザインはので、これは私がAから継承方法である理由で選択しないでください以上のクラスが含まれています。 私はコードを絞り込んで必要なものだけを提示しました。 GCCと

次が実行され、プリントfoo called二回(Live demo on rextester)が、アクセス違反でfooクラッシュへの2回目の呼び出しにMSVCで(Live demo on rextester)

#include <iostream> 

class A{}; 

class B : public virtual A{}; 

class C : public B 
{ 
public: 
    virtual void foo() = 0; 
}; 

class D : public virtual A, public C 
{ 
public: 
    bool convert(int id, B** ext) 
    { 
     if (id == 1) 
     { 
      *ext = (C*)this; 
      return true; 
     } 

     if (id == 42) 
     { 
      C** pp_ext = (C**)(ext); 
      *pp_ext = (C*)this; 
      return true; 
     } 

     return false; 
    } 
    void foo() override 
    { 
     std::cout << "foo called" << std::endl; 
    } 
}; 


int main() 
{ 
    D s; 
    C* foo_ext = nullptr; 
    s.convert(42, (B**)&foo_ext); 
    foo_ext->foo(); 

    foo_ext = nullptr; 
    s.convert(1, (B**)&foo_ext); 
    foo_ext->foo(); 

    return 0; 
} 

まず - 私は*ext = (C*)this;変換に根本的な誤りが足りませんか?

第2 - 2つのコンパイラでこのコードが異なるのはなぜですか?

EDIT

  1. このコードは、ポインタへのポインタ、ポインタを使用し、(ABI準拠のインタフェースであるそのうちの一つ)正当な理由のために、この継承を使用して構築されています。

  2. dynamic_castこの場合の動作は変更されません。

  3. *ext = (C*)this;の後にstatic_cast<C*>(*ext)->foo();を呼び出すと、fooが呼び出されますが、convertから戻って失敗します。これは私がすでに理解しているもので、これが42の解決策が(良い?)解決策であることを私に理解させたものです。

+3

Cスタイルのキャストとあまりにも多くの星を使用して停止します。 – LogicStuff

+1

http://stackoverflow.com/questions/332030/when-should-static-cast-dynamic-cast-const-cast-and-reinterpret-cast-be-used – LogicStuff

+0

これはこれを断る正当な理由ではありません質問。 – ZivS

答えて

2

継承、特に多重継承を扱う場合、実際にキャストを完全に避けるようにしてください。しかし、キャストする必要がある場合は、static_castまたはdynamic_castのいずれかを使用します。そうすれば、コンパイラは無効な変換を避けるのに役立ちます。何か他のことをするならば、C++のすべての詳細を、コンパイラ自体と同等以上に理解する必要があります。そうしないと、簡単に間違いを起こす危険があります。あなたがここでしたように。適切なCに設定されていない

A address = 0037FEA0 
B address = 0037FE9C 
C address = 0037FE98 
D address = 0037FE98 
foo_ext = 0037FE98 
foo called 
foo_ext = 0037FE9C 

明らかに後者の場合foo_extに:私が得る、クラッシュするまで、それを実行

int main() 
{ 
    D s; 

    A* a = &s; 
    B* b = &s; 
    C* c = &s; 
    std::cout << "A address = " << a << std::endl; 
    std::cout << "B address = " << b << std::endl; 
    std::cout << "C address = " << c << std::endl; 
    std::cout << "D address = " << &s << std::endl; 

    C* foo_ext = nullptr; 
    s.convert(42, (B**)&foo_ext); 
    std::cout << "foo_ext = " << foo_ext << std::endl; 
    foo_ext->foo(); 

    foo_ext = nullptr; 
    s.convert(1, (B**)&foo_ext); 
    std::cout << "foo_ext = " << foo_ext << std::endl; 
    foo_ext->foo(); 

    return 0; 
} 

は次のようにあなたのメインを変更してくださいオブジェクトのアドレスではなく、B部分です。実際には、foo()への呼び出しは、間違ったまたは存在しない仮想テーブルポインタを経由している可能性があります。したがって、クラッシュします。

なぜ、最初のケースが機能するのですか?さて、何が起こっているかの最低限にそれを減らし、あなたが効果的に行われてきた:

C* foo_ext = nullptr; 
C** ppc = &foo_ext; 
B** ppb = (B**)ppc; 
C** pp_ext = (C**)ppb; 
*pp_ext = &s; 

をだから、Cポインタで始まり、Cポインタで終わります。そして、コンパイラはDポインタをCポインタに正しくシフトする方法を知っています。 (ベースへの変換派生コンパイラはそれを行うには、キャストを必要としません。)

しかし、あなたが効果的に持っている後者の場合には:

C* foo_ext = nullptr; 
C** ppc = &foo_ext; 
B** ext = (B**)ppc; 
*ext = &s; 

だから、最後の行でコンパイラがDをシフトしていますポインタはBポインタになります。しかし実際にはそのアドレスはCポインタで終わってしまいます。それでは、継承階層をあまりにも遠くに戻してしまったのです!


は今、あなたのプログラムを修正する方法...まあ、当然の基本的な考え方は、鋳物のほとんどを取り除くと、静的または動的キャストと残りのカップルのスポットを置き換えることです。以下を試してください:

#include <iostream> 

class A {}; 

class B : public virtual A {}; 

class C : public B 
{ 
public: 
    virtual void foo() = 0; 
}; 

class D : public virtual A, public C 
{ 
public: 
    bool convert(int id, B** ext) 
    { 
     if (id == 1) 
     { 
      *ext = this; 
      return true; 
     } 

     if (id == 42) 
     { 
      *ext = this; 
      return true; 
     } 

     return false; 
    } 
    void foo() override 
    { 
     std::cout << "foo called" << std::endl; 
    } 
}; 


int main() 
{ 
    D s; 

    A* a = &s; 
    B* b = &s; 
    C* c = &s; 
    std::cout << "A address = " << a << std::endl; 
    std::cout << "B address = " << b << std::endl; 
    std::cout << "C address = " << c << std::endl; 
    std::cout << "D address = " << &s << std::endl; 

    B* b_pointer = nullptr; 
    C* foo_ext = nullptr; 
    s.convert(42, &b_pointer); 
    foo_ext = static_cast<C*>(b_pointer); 
    std::cout << "foo_ext = " << foo_ext << std::endl; 
    foo_ext->foo(); 

    b_pointer = nullptr; 
    foo_ext = nullptr; 
    s.convert(1, &b_pointer); 
    foo_ext = static_cast<C*>(b_pointer); 
    std::cout << "foo_ext = " << foo_ext << std::endl; 
    foo_ext->foo(); 

    return 0; 
} 

これは、すべての期待される出力で正常に動作するはずです。静的キャストを使用すると、コンパイラは型ごとにポインタを適切に配置できます。 (この限定された例ではもちろん、すべての型が本当に何であるかを知っているので、動的キャストを気にする必要はありません)

この例は現実世界の問題に基づいていると仮定します。私はこの正確な解決策がそれに適しているかどうかは言えません(明らかに私は実際の状況を知らないので)。しかし、原則は健全です。疑わしいキャストを強制することによってコンパイラに嘘をつけないでください。可能な限り自動的に変換させ、必要に応じてstatic_castまたはdynamic_castに限定してください。

+0

詳細な答えをいただきありがとうございます。また、実際の問題は呼び出し前にB **にキャストされているので、多重継承なしでこの動作を再現しました。コンパイラの違いの原因は何ですか? – ZivS

+0

@ZivS異なるコンパイラは、オブジェクトのパーツのメモリレイアウトに関して異なる選択をすることができます。特にメンバーのないクラスを持つと、オブジェクトの基底部分が派生オブジェクトの合計サイズ内にスペースを占めない最適化が可能になります。だから、他の作品では、派生してベースとなるポインタは同じアドレスになります。私の結果には、CとDのアドレスを持つインスタンスがあることに注目してください。おそらくg ++上でBとCのために起こるので、あなたのコードは悪いキャストにもかかわらず動作してしまいます。私はそれに頼りたくはありません。 – TheUndeadFish

+0

@ZivSこのコードにはどこにMIがありますか? – curiousguy

1

最初の実行時の問題がでてくる:

s.convert(42, (B**)&foo_ext); 

foo_extC *型を持ちます。したがって、*extconvertの内部で使用すると、メモリがC *のようにB *のようにアクセスすることによって、厳密なエイリアシング違反が行われます。一般的に、異なるタイプのポインタは、サイズと表現が異なる場合があります。たとえそうでなくても、それらをエイリアスすることはまだ許されていません。

厳密なエイリアシング規則には基本クラスにアクセスするための例外がありますが、基本クラスポインタには拡張されません。

MSVCはその状況で厳密なエイリアシングを強制しません(実際には、Windows APIのいくつかのことがその動作に依存しています)。しかし、移植性のあるコードを作成したい場合は、厳密なエイリアシング違反に頼らないことをお勧めします。


convert関数は、バイパスポインタを使用します。これはC++では決して必要ありません。代わりに、参照渡しを言語機能として使用することができます。これにより、間違いを起こす可能性が減ります。実際には、コードの間違いの一部は実際には参照表記でも表現できません。

#include <iostream> 

class A{}; 

class B : public virtual A{}; 

class C : public B 
{ 
public: 
    virtual void foo() = 0; 
}; 

class D : public virtual A, public C 
{ 
public: 
    bool convert(int id, B*& ext) 
    { 
     if (id == 1) 
     { 
      ext = static_cast<C *>(this); // note: redundant cast 
      return true; 
     } 

     if (id == 42) 
     { 
      ext = this; 
      return true; 
     } 

     return false; 
    } 
    void foo() override 
    { 
     std::cout << "foo called" << std::endl; 
    } 
}; 

int main() 
{ 
    D s; 
    B* foo_ext_b; 
    C* foo_ext; 

    foo_ext_b = nullptr; 

    if (!s.convert(42, foo_ext_b)) 
     throw std::runtime_error("convert42 failed"); 

    foo_ext = static_cast<C *>(foo_ext_b); 
    foo_ext->foo(); 

    foo_ext_b = nullptr; 

    if (!s.convert(1, foo_ext_b)) 
     throw std::runtime_error("convert1 failed"); 

    foo_ext = static_cast<C *>(foo_ext_b); 
    foo_ext->foo(); 

    return 0; 
} 

foo_ext = static_cast<C *>(foo_ext_b);を使用して、一般的に、誤りがちであること:ここ

は、変更されたバージョンです。 convertが返された場合、のインスタンスの基本クラスであるBを指していない場合、未定義の静かな動作が発生します。

安全のため、代わりにdynamic_castを使用します。しかし、dynamic_castを動作させるには、基本クラスに少なくとも1つの仮想関数が必要です。 BまたはAに仮想デストラクタを追加できます。

+0

クラスへのポインタは同じサイズを持つ必要があります。 – curiousguy