0

gcc / g ++の使用4.6.1これはコンパイラのバグですか、それとも言語機能ですか?コンパイラーは私に怒鳴りませんでしたが、それは少なくともコンパイラーの欠点だと思います。

new演算子がオーバーロードされた親クラスがあります:

class C{ // just here to be faithful to the original code
  int y;
}

class A{
public:
  void* operator new(size_t enfacia_size, uint count){
      size_t total_size 
      = enfacia_size
      + item::size() * count; // the 'tail'
      ;
      this_type *new_pt = (this_type *)malloc(total_size);
      new_pt->count = count;
      return new_pt;
  }
  uint count;
}

class B : public C, public A{
public:
    int i;
};

オブジェクト自体は可変長であるため、オブジェクトの長さを知る必要があります。したがって、countフィールドがあります。この呼び出しでは、カウントはオフセット0で検出されます。

...
new A *pt = new(10) A;  // offset_of(A,count)==0
new B *pt = new(10) B;  // offset_of(B,count)==4

これが問題です。演算子の内部ではnew、の値countは、親から呼び出されたか子から呼び出されたかに関係なく、常にオフセット0に書き込まれます。したがって、継承で使用すると、プログラムがクラッシュします..静的メソッドと継承に問題はありますか?これで何が起こっているのですか?

4

8 に答える 8

3

あなたが提案するソリューションは、そのクラスの有効期間外の非PODクラスのメンバーにアクセスすることにより、未定義の動作を呼び出します。

C++2003 の §3.8 から読むと、

型 T のオブジェクトの有効期間は、次の時点で始まります。

  • タイプ T に適した位置合わせとサイズのストレージが取得されます。
  • T が非自明なコンストラクタ (12.1) を持つクラス型である場合、コンストラクタの呼び出しは完了しています。

Bオブジェクトには重要なコンストラクターがあるため、ライフタイムは のコンストラクターが完了した後に始まりますBAのコンストラクターはB、 のコンストラクターが完了する前に実行されるため、 の有効期間の前に実行されBます。

オブジェクトの存続期間が始まる前で、オブジェクトが占有するストレージが割り当てられた後...オブジェクトが配置されるストレージの場所を参照するポインタは、限定された方法でのみ使用できます. …オブジェクトが[特定の条件を満たす場合、あなたの場合はそうです]、プログラムは次の場合に未定義の動作をします。

  • ポインターは、非静的データ メンバーにアクセスするか、オブジェクトの非静的メンバー関数を呼び出すために使用されます。
  • ポインターが基本クラス型へのポインターに暗黙的に変換される (4.10)、または
  • ポインターは static_cast (5.2.9) のオペランドとして使用されます (ただし、変換が void* または void* に続いて char* または unsigned char* に変換される場合を除く)。
  • ポインターは、dynamic_cast (5.2.7) のオペランドとして使用されます。

したがって、これら 4 つの条件のいずれかに違反するソリューションを提案すると、未定義の動作が発生します。具体的には、質問のコードはポインターを のオペランドとしてstatic_cast使用し、それを使用して非静的データメンバーにアクセスします。


とはいえ、C++ 標準では定義されていませんが、特定のコンパイラで有効なプログラムを次に示します。つまり、これは C++ プログラムではありませんが、MSVC++ プログラムまたは G++ プログラムである可能性があります。

#include <iostream>
#include <cstdlib>
class C{ // just here to be faithful to the original code
  int y;
};

template <class this_type>
class A {
public:
  void* operator new(size_t enfacia_size, unsigned int count){
      size_t total_size
      = enfacia_size
      + 42 * count; // the 'tail'
      ;
      this_type *new_pt = (this_type *)malloc(total_size);
      new_pt->count = count;
      return new_pt;
  }
  unsigned int count;
};

class B : public C, public A<B>{
public:
    int i;
};

int main () {
  B *b = new(10) B;
  std::cout << b->count << "\n";
}
于 2012-05-01T19:20:06.723 に答える
0

あなたは「継承のためにフィールドが移動する」という奇妙な主張をしていますが、そのようなことは起こりません。

このプログラムを試してください:

#include <iostream>
#include <stdint.h>

struct C
{
    int i;
};

struct A
{
    int count;

    void f()
    {
        uintptr_t pt = reinterpret_cast<uintptr_t>(this);
        uintptr_t pc = reinterpret_cast<uintptr_t>(&count);
        std::cout << "Offset of A::count within A is " << (pc - pt) << '\n';
    }
};

struct B : C, A
{
    void g()
    {
        uintptr_t pt = reinterpret_cast<uintptr_t>(this);
        uintptr_t pc = reinterpret_cast<uintptr_t>(&count);
        std::cout << "Offset of A::count within B is " << (pc - pt) << '\n';
    }
};

int main()
{
    A a;
    a.f();
    B b;
    b.f();
    b.g();
}

これにより、次のものが生成されます。

Offset of A::count within A is 0
Offset of A::count within A is 0
Offset of A::count within B is 4

タイプBのオブジェクトまたはタイプAのオブジェクトでA::fを呼び出すかどうかは関係ありません。それでも、A :: fであり、固定オフセットでA:countにアクセスします。B :: gを呼び出すと関数はA::countメンバーがとは異なるオフセットにあることを認識します。this

同じことがあなたoperator newにも起こります。その関数はAのメンバーであり、Bについて何も知りません。this_type::countあなたがアクセスするとき、あなたはにアクセスしていますA::count。あなたがそれをBと呼ぶことは無関係であり、A::operator new存在するだけです(あなたが思っているように関数はコピーされて生成B::operator newされません)。

テンプレートバージョンが機能するのは、タイプthis_typeがAではなくBを参照しているため、関数がアクセスするためです。B::count

于 2012-05-04T12:54:35.893 に答える
0

このコードには、ローカル型にキャストされたメソッドに void ポインターもあります。

#include <cstddef>
#include <stdlib.h>
typedef unsigned int uint;

class C{ // just here to be faithful to the original code
  int y;
};


class A{
public:
  typedef A this_type;

  void* method(uint count){
    void *vp;
    vp = (void *)this;
    this_type *p;
    p = (this_type *)vp;
    p->count = count;   
  };

  uint count;
};

class B : public C, public A{
public:
    typedef B this_type;
    int i;
};

int main(){
  int j;

  A a;
  a.method(5);
  j++;

  B b;
  b.method(5);
  j++;

};

そして、期待どおりに動作します:

(gdb) b main
Breakpoint 1 at 0x80484a1: file try_offsets_2.ex.cc, line 36.
(gdb) r
Starting program: /try_offsets_2 
Breakpoint 1, main () at try_offsets_2.ex.cc:36
(gdb) n
(gdb) 
$1 = (A *) 0xbffff758
(gdb) x /10x &a
0xbffff758:    0x00000005    0x003dbff4    0x08048500    0x003dbff4
0xbffff768:    0x00000000    0x0027d113    0x00000001    0xbffff804
0xbffff778:    0xbffff80c    0x0012eff4
(gdb) n
(gdb) x /10x &b
0xbffff74c:    0x003dc324    0x00000005    0x00296c55    0x00000005
0xbffff75c:    0x003dbff5    0x08048500    0x003dbff4    0x00000000
0xbffff76c:    0x0027d113    0x00000001
(gdb)

設定された子のカウントを確認できます。はい、私はこれがどのように機能するかを知っています。もちろん、これは期待される結果です。その理由は、method() が呼び出されたときに「this」ポインターが正しく設定されているためです。

ここでの元の質問は、演算子 new が子の期待される結果を生成せず、代わりに親型のように動作することです。それでも、代入を含む演算子 new は、コンパイラからのノイズなしで継承されます。

これらすべてに対する本当の答えは、 this ポインターの幻の最初のオペランドが渡されないという点で、 new 演算子が他のメソッドと異なることだと思います。そして、オブジェクトがまだ存在しないため、上記で nm が指摘したように、どうすればよいでしょうか。型定義は既に存在しますが、コンパイラが多重継承で型を子メソッドに伝達する手段は、メソッドが定義された型のオブジェクトの適切な領域を指すように設定されたこのポインターをファントムの最初のオペランドを介して行います。 .

コンパイラが this ポインターのように this_type を指定した場合、演算子 new は、void * をキャストするだけで、作成した割り当て内のフィールドにアクセスできます。これは多くの状況で非常に役立ちます。

しかし、私に残っている最終的な疑問は、元のコードに示されているように、なぜコンパイラが不正な代入に対してエラーを出さず、子に対して間違った値を生成するコードを生成するのかということです。(そしてジョン、はい、私は子に new を呼び出し、親としてカウントを割り当て、警告などはありません。これは、通常のプログラマーの目には予期しない結果であると言っても過言ではありません)。

于 2012-05-05T00:38:27.740 に答える
0

別の解決策は次のとおりです。

#include <cstddef>
#include <stdlib.h>

typedef unsigned int uint;

class C{ // just here to be faithful to the original code
  int y;
};

class A{
public:
  uint count;
};

class B : public C, public A{
public:
    int i;
};

template<typename T>
struct A_allocation_helper : T
{
  void* operator new(size_t enfacia_size, uint count){
    size_t total_size
      = enfacia_size
      + sizeof(int) * count; // the 'tail'
      ;
    T *new_pt = (T *)malloc(total_size);
    new_pt->count = count;
    return new_pt;
  };

};


int main(){
  B *b_pt = new(5) A_allocation_helper<B>;
  return b_pt->count;
};

現在、メモリ割り当てを行うコードは、ベースについてのみ知っている型によって行われるのではなく、正しい型を知っています。

于 2012-05-04T23:42:19.470 に答える
0

特に多重継承が関係している場合、基本クラスと派生クラスの間でオフセットが異なるのは正常です。コンパイラは、ある型から別の型に変換するたびにポインター アドレスを変更するための非表示のフィックスアップを提供します。

各ポインター型は、それが指すオブジェクトの種類で一貫している必要があるため、このようにする必要があります。A* p1 = new Ap1とA* p2 = new Bp2 に同じオフセットを使用する必要があります。

于 2012-05-01T19:26:20.003 に答える
0

これはうまくコンパイルされます:

#include <cstddef>
#include <stdlib.h>
typedef unsigned int uint;

class C{ // just here to be faithful to the original code
  int y;
};

class A{
public:
  typedef A this_type;

  void* operator new(size_t enfacia_size, uint count){
      size_t total_size 
    = enfacia_size
    + sizeof(int) * count; // the 'tail'
    ;
      this_type *new_pt = (this_type *)malloc(total_size);
      new_pt->count = count;
      return new_pt;
  };
  uint count;
};

class B : public C, public A{
public:
    int i;
};

int main(){
  B *b_pt = new(5) B;  
  uint j=0;
  j++;
};

gdbに表示される「問題」は次のとおりです。

(gdb) b main
Breakpoint 1 at 0x80484e1: file try_offsets.ex.cc, line 32.
(gdb) r
Starting program try_offsets 
Breakpoint 1, main () at try_offsets.cc:32
(gdb) n
(gdb) p &(b_pt->count)
$1 = (uint *) 0x804a00c
(gdb) x/10 b_pt
0x804a008:  5   0   0   0
0x804a018:  0   0   0   0
0x804a028:  0   135129
(gdb) p b_pt
$2 = (B *) 0x804a008
(gdb) 

count は 0x804a00c にありますが、count に代入すると 0x804a008 に書き込まれることに注意してください。このタイプがテンプレートを介して子に設定されているRobによって指摘されたバリエーションがあります:

#include <cstddef>
#include <stdlib.h>
typedef unsigned int uint;

class C{ // just here to be faithful to the original code
  int y;
};

template<typename this_type>
class A{
public:

  void* operator new(size_t enfacia_size, uint count){
      size_t total_size 
    = enfacia_size
    + sizeof(int) * count; // the 'tail'
    ;
      this_type *new_pt = (this_type *)malloc(total_size);
      new_pt->count = count;
      return new_pt;
  };
  uint count;
};

class B : public C, public A<B>{
public:
    int i;
};

int main(){
  B *b_pt = new(5) B;  
  uint j=0;
  j++;
};

正しい動作が得られます。

(gdb) b main
Breakpoint 1 at 0x80484e1: file try_offsets.ex.cc, line 32.
(gdb) r
Starting program
Breakpoint 1, main () at try_offsets.cc:32
(gdb) n
(gdb) p &(b_pt->count)
$1 = (uint *) 0x804a00c
(gdb) x/10 b_pt
0x804a008:  0   5   0   0
0x804a018:  0   0   0   0
0x804a028:  0   135129
(gdb) 

ただし、興味深いことに、このソリューションでは、this_type が「A」に設定されているとコンパイルされません。

class B : public C, public A<A>{
public:
    int i;
};

与えます:

try_offsets.cc:26:31: error: type/value mismatch at argument 1 in template parameter list for ‘template<class this_type> class A’
try_offsets.cc:26:31: error:   expected a type, got ‘A’

私が落ち着いた解決策:

#include <cstddef>
#include <stdlib.h>
typedef unsigned int uint;

class C{ // just here to be faithful to the original code
  int y;
};


class A{
public:
  typedef A this_type;

  A(uint count):count(count){;}

  void* operator new(size_t enfacia_size, uint count){
      size_t total_size 
    = enfacia_size
    + sizeof(int) * count; // the 'tail'
    ;
      this_type *new_pt = (this_type *)malloc(total_size);
      return new_pt;
  };
  uint count;
};

class B : public C, public A{
public:
    B(uint count):A(count){;}
    int i;
};

int main(){
  B *b_pt = new(5) B(5);  
  uint j=0;
  j++;
};

これは古い慣習を破りますが。アロケーターが割り当てポイントのすぐ上に割り当ての長さを書き込むのは正常です。つまり、たとえば、delete/free がヒープ上のブロックの長さをどのように認識しているかです。また、演算子 delete は引数を取ることができないため、これが情報を取得する方法です。

于 2012-05-03T07:15:57.177 に答える
0

これはおそらく機能しません。operator new割り当てられているオブジェクトのタイプを認識しません。サイズのみです。オペレーターを直接呼び出すことができるため、オブジェクトがまったく存在しない可能性があります。

各派生クラスで異なる定義this_typeを行っても (プレゼンテーションでこの部分を見逃していると思います)、基本クラス メソッドでの意味には影響しません。

于 2012-05-01T18:02:57.913 に答える
0

はい、多重継承でフィールドを移動します。元の例と同じコード形式で、移動フィールドを示す例を次に示します。唯一の重要な違いは、メソッドが「operator new」ではなく「method」と呼ばれることです。コード例の後に、フィールドが移動したことを示すデバッガー出力が表示され、移動してもフィールドに正しく割り当てられた値が表示されます。

#include <cstddef>
#include <stdlib.h>
typedef unsigned int uint;

class C{ // just here to be faithful to the original code
  int y;
};


class A{
public:
  typedef A this_type;

  void* method(uint count){
    void *vp;
    vp = (void *)this;
    this_type *p;
    p = (this_type *)vp;
    p->count = count;   
  };

  uint count;
};

class B : public C, public A{
public:
    typedef B this_type;
    int i;
};

int main(){
  int j;

  A a;
  a.method(5);
  j++;

  B b;
  b.method(5);
  j++;

};

デバッガ出力:

(gdb) b main
Breakpoint 1 at 0x80484a1: file try_offsets_2.ex.cc, line 36.
(gdb) r
Starting program: /try_offsets_2 
Breakpoint 1, main () at try_offsets_2.ex.cc:36
(gdb) n
(gdb) 
$1 = (A *) 0xbffff758
(gdb) x /10x &a
0xbffff758: 0x00000005  0x003dbff4  0x08048500  0x003dbff4
0xbffff768: 0x00000000  0x0027d113  0x00000001  0xbffff804
0xbffff778: 0xbffff80c  0x0012eff4
(gdb) n
(gdb) x /10x &b
0xbffff74c: 0x003dc324  0x00000005  0x00296c55  0x00000005
0xbffff75c: 0x003dbff5  0x08048500  0x003dbff4  0x00000000
0xbffff76c: 0x0027d113  0x00000001
(gdb) 

親のオフセット 0 にあった 5 は、子のオフセット 4 にあることに注意してください。また、親メソッドは子に継承されましたが、その型が更新されたため、オフセット 4 のフィールドに正しく書き込まれたことにも注意してください。

于 2012-05-04T20:36:01.413 に答える