1

Inside the C++ Object Modelを読んでいます。セクション 1.3 で

では、なぜそうなのかというと、

Bear b; 
ZooAnimal za = b; 

// ZooAnimal::rotate() invoked 
za.rotate(); 

呼び出されたrotate()のインスタンスはBearのインスタンスではなく、ZooAnimalのインスタンスですか? さらに、メンバごとの初期化によって、あるオブジェクトの値が別のオブジェクトにコピーされる場合、za の vptr が Bear の仮想テーブルをアドレス指定しないのはなぜですか?

2 番目の質問に対する答えは、あるクラス オブジェクトの初期化と別のクラス オブジェクトへの割り当てにコンパイラが介入することです。コンパイラは、オブジェクトに 1 つ以上の vptr が含まれている場合、それらの vptr 値がソース オブジェクトによって初期化または変更されないようにする必要があります。

そこで、以下のテストコードを書きました。

#include <stdio.h>
class Base{
public:
    virtual void vfunc() { puts("Base::vfunc()"); }
};
class Derived: public Base
{
public:
    virtual void vfunc() { puts("Derived::vfunc()"); }
};
#include <string.h>

int main()
{
    Derived d;
    Base b_assign = d;
    Base b_memcpy;
    memcpy(&b_memcpy, &d, sizeof(Base));

    b_assign.vfunc();
    b_memcpy.vfunc();

    printf("sizeof Base : %d\n", sizeof(Base));

    Base &b_ref = d;
    b_ref.vfunc();

    printf("b_assign: %x; b_memcpy: %x; b_ref: %x\n", 
        *(int *)&b_assign,
        *(int *)&b_memcpy,
        *(int *)&b_ref);
    return 0;
}

結果_

Base::vfunc()
Base::vfunc()
sizeof Base : 4
Derived::vfunc()
b_assign: 80487b4; b_memcpy: 8048780; b_ref: 8048780

私の質問は、b_memcpy がまだ Base::vfunc() と呼ばれている理由です。

4

4 に答える 4

2

あなたがしていることは C++ 言語では違法です。つまり、b_memcpyオブジェクトの動作は未定義です。後者は、どのような行動も「正しく」、あなたの期待は完全に根拠がないことを意味します。未定義の動作を分析しようとしてもあまり意味がありません。論理に従う必要はありません。

実際には、実際に didの仮想テーブル ポインタをオブジェクトmemcpyにコピーした可能性は十分にあります。そして、あなたの実験はそれを確認します。ただし、(call の場合のように) 即時オブジェクトを介して仮想メソッドが呼び出されると、ほとんどの実装は仮想テーブルへのアクセスを最適化し、ターゲット関数への直接(非仮想) 呼び出しを実行します。言語の正式な規則では、法的な措置によって以外のものへのディスパッチを呼び出すことはできないと規定されています。これが、コンパイラがこの呼び出しを への直接呼び出しに安全に置き換えることができる理由です。これが、通常、仮想テーブルの操作が呼び出しに影響を与えない理由です。Derivedb_memcpyb_refb_memcpy.vfunc()b_memcpy.vfunc()Base::vfunc()Base::vfunc()b_memcpy.vfunc()

于 2016-12-12T04:29:59.190 に答える
0

あなたはすることを許可されていません

memcpy(&b_memcpy, &d, sizeof(Base));

- とは「プレーンな古いデータ」オブジェクトb_memcpyではないため(仮想メンバー関数があるため)、未定義の動作です。d

あなたが書いた場合:

b_memcpy = d;

その後、期待どおりに印刷Base::vfunc()されます。

于 2016-12-12T04:31:08.857 に答える
0

vptr の使用は標準の範囲外です。

確かに、memcpyここでの使用にはUBがあります

の使用memcpy、または非 POD、つまり vptr を持つオブジェクトのその他のバイト操作には未定義の動作があることを指摘する回答は、厳密に技術的に正しいですが、質問には答えません。質問は、標準によって義務付けられていない vptr (vtable ポインター) の存在に基づいています。もちろん、答えには標準外の事実が含まれ、結果の請求書は標準によって保証されません!

標準テキストは、vptr に関しては関係ありません

問題は、vptr の操作が許可されていないということではありません。標準テキストに記述さえされていないものを操作することが標準によって許可されているという考えはばかげています。もちろん、vptr を変更する標準的な方法は存在しませんが、これは重要ではありません。

vptr は、ポリモーフィック オブジェクトの型をエンコードします。

ここでの問題は、標準が vptr について述べていることではありません。問題は vptr が何を表しているか、そしてそれについて標準が何を述べているかです。vptr はオブジェクトの動的な型を表します。演算の結果が動的型に依存する場合は常に、コンパイラは vptr を使用するコードを生成します。

[MI に関する注意: 「その」vptr (vptr が 1 つだけであるかのように) と言いますが、MI (多重継承) が関係する場合、オブジェクトは複数の vptr を持つことができ、それぞれが特定のポリモーフィックな基本クラスと見なされる完全なオブジェクトを表します。タイプ。(ポリモーフィック クラスとは、少なくとも 1 つの仮想関数を持つクラスです。)]

[仮想ベースに関する注意: vptr についてのみ言及しますが、仮想ベース サブオブジェクトの場所など、動的型の側面を表すために他のポインターを挿入するコンパイラもあれば、その目的で vptr を使用するコンパイラもあります。vptr について正しいことは、これらの他の内部ポインターについても正しいです。]

したがって、vptr の特定の値は動的な型に対応します。これは、最も派生したオブジェクトの型です。

存続期間中のオブジェクトの動的タイプの変更

構築中に動的型が変更されるため、コンストラクター内からの仮想関数呼び出しは「驚くべき」ものになる可能性があります。構築中に仮想関数を呼び出す規則は特別だと言う人もいますが、絶対にそうではありません。そのオーバーライドは、構築された最も派生したオブジェクトに対応するクラスであり、コンストラクターC::C(arg-list)では、常にクラスの型ですC

破棄中、動的型は逆の順序で変更されます。デストラクタ内からの仮想関数の呼び出しは、同じ規則に従います。

何かが未定義のままになっている場合の意味

標準で認可されていない低レベルの操作を行うことができます。動作が C++ 標準で明示的に定義されていないということは、それが他の場所で説明されていないことを意味するものではありません。操作の結果が C++ 標準で UB (未定義の動作) を持つように明示的に記述されているからといって、実装でそれを定義できないわけではありません。

コンパイラの動作方法に関する知識を使用することもできます。厳密な個別コンパイルが使用される場合、つまり、コンパイラが個別にコンパイルされたコードから情報を取得できない場合、個別にコンパイルされたすべての関数は「ブラック ボックス」になります。この事実を利用できます。コンパイラは、個別にコンパイルされた関数が実行できることはすべて実行されると想定する必要があります。特定の関数内であっても、asmディレクティブを使用して同じ効果を得るasmことができます。制約のないディレクティブは、C++ で有効なすべてのことを行うことができます。その効果は、「その時点でコード分析から知っていることを忘れる」という指示です。

標準では、何が動的型を変更できるかが説明されており、構築/破壊以外は何も変更できないため、「外部」(ブラックボックス) 関数のみが構築/破壊の実行を許可され、動的型を変更できます。

[basic.life]/8を参照してください。

オブジェクトの有効期間が終了した後、オブジェクトが占有していたストレージが再利用または解放される前に、元のオブジェクトが占有していたストレージの場所に新しいオブジェクトが作成された場合、元のオブジェクトを指すポインタ、その参照または、元のオブジェクトの名前が自動的に新しいオブジェクトを参照し、新しいオブジェクトの有効期間が開始されると、新しいオブジェクトを操作するために使用できます。

(8.1) 新しいオブジェクトのストレージは、元のオブジェクトが占めていたストレージの場所を正確にオーバーレイします。

(8.2) 新しいオブジェクトが元のオブジェクトと同じ型である (最上位の cv 修飾子を無視する)、および

(8.3) 元のオブジェクトの型が const 修飾されておらず、クラス型の場合、その型が const 修飾または参照型である非静的データ メンバーを含まない。

(8.4) 元のオブジェクトは型 T の最派生オブジェクト ([intro.object]) であり、新しいオブジェクトは型 T の最派生オブジェクトです (つまり、これらは基本クラスのサブオブジェクトではありません)。

これは、コンストラクターを (placement new で) 呼び出して、オブジェクト (名前、ポインターなど) を指定するために使用したのと同じ式を引き続き使用できる唯一のケースは、動的型が変更されない場合であることを意味します。そのため、vptr は同じままです。

つまり、低レベルのトリックを使用して vptr を上書きしたい場合は、次のことができます。ただし、同じ値を書き込む場合のみ

つまり、vptr をハッキングしようとしないでください。

于 2017-01-28T06:15:16.830 に答える