7

(これは、未定義の動作 (UB) に関する別の質問です。このコードが何らかのコンパイラで「機能する」場合、それは UB の土地では何の意味もありません。それは理解されています。しかし、正確には、以下のどの行で UB に交差するのでしょうか?)

(SO には非常によく似た質問が既にいくつかあります。たとえば、(1) ですが、ポインターを逆参照する前に、ポインターを安全に使用できることに興味があります。)

非常に単純な基本クラスから始めます。virtualメソッドはありません。継承なし。(おそらく、これは POD であるすべてのものに拡張できますか?)

struct Base {
        int first;
        double second;
};

次に、(非virtual)メソッドを追加し、メンバーを追加しない単純な拡張。virtual継承なし。

struct Derived : public Base {
        int foo() { return first; }
        int bar() { return second; }
};

次に、次の行を検討してください。定義された動作からの逸脱がある場合、正確にどの行を知りたいと思います。私の推測では、ポインターに対して多くの計算を安全に実行できると思います。これらのポインター計算の一部は、完全に定義されていない場合でも、少なくとも完全に役に立たないわけではない「不確定/未指定/実装定義」の値を与える可能性はありますか?

void foo () {
    Base b;
    void * vp = &b;     // (1) Defined behaviour?
    cout << vp << endl; // (2) I hope this isn't a 'trap value'
    cout << &b << endl; // (3a) Prints the same as the last line?
                        // (3b) It has the 'same value' in some sense?
    Derived *dp = (Derived*)(vp);
                        // (4) Maybe this is an 'indeterminate value',
                        // but not fully UB?
    cout << dp << endl; // (5)  Defined behaviour also?  Should print the same value as &b

編集: プログラムがここで終了した場合、それは UB でしょうか? この段階では、ポインター自体を出力に出力する以外に、 で何もしようとしていないことに注意してください。dp単にキャスティングがUBなら、質問はここで終わると思います。

                        // I hope the dp pointer still has a value,
                        // even if we can't dereference it
    if(dp == &b) {      // (6) True?
            cout << "They have the same value. (Whatever that means!)" << endl;
    }

    cout << &(b.second) << endl; (7) this is definitely OK
    cout << &(dp->second) << endl; // (8)  Just taking the address. Is this OK?
    if( &(dp->second) == &(b.second) ) {      // (9) True?
            cout << "The members are stored in the same place?" << endl;
    }
}

上記の(4)については少し神経質です。しかし、void ポインターとの間でキャストすることは常に安全だと思います。おそらく、そのようなポインタの値について議論することができます。しかし、キャストを行い、ポインタを出力するように定義されていますcoutか?

(6)も重要です。これは true と評価されますか?

(8)では、このポインターが初めて逆参照されます (正しい用語?)。ただし、この行は から読み取らないことに注意してくださいdp->second。これはまだ単なる左辺値であり、そのアドレスを取得します。このアドレスの計算は、C 言語から得た単純なポインター算術規則によって定義されていると思います。

上記のすべてに問題がなければ、それが問題ないことを証明できstatic_cast<Derived&>(b)、完全に使用可能なオブジェクトにつながる可能性があります。

4

2 に答える 2

1

(厳密なエイリアシングの観点から、私自身の質問に答えようとしています。優れたオプティマイザーは、予想外のことを行う資格があり、それが効果的にUBを提供します。しかし、私は決して専門家ではありません!)

この関数では、

 void foo(Base &b_ref) {
     Base b;
     ....
 }

bとがb_ref相互に参照できないことは明らかです。この特定の例には、互換性のある型の分析は含まれていません。新しく構築されたローカル変数がそれ自体への唯一の参照であることが保証されているという単純な観察です。これにより、オプティマイザーはいくつかのトリックを実行できます。レジスタに格納し、影響を受けない知識で安全にを変更するbなどのコードを実行できます。(おそらく本当に賢いオプティマイザーだけがこれに気付くでしょうが、それは許されています。)b_ref.modify()b_refb

次に、これを検討します。

void foo(Base &b_ref, Derived&d_ref);

この関数の実装内では、最適化はb_ref と d_ref が異なるオブジェクトを参照していると想定できません。したがって、コードが を呼び出す場合d_ref.modify()、次にコードがアクセスするときに、オブジェクトb_refを格納するメモリを再度調べる必要がありb_refます。CPU レジスタにデータのコピーがある場合は、b_ref古いデータである可能性があります。

しかし、タイプが互いに何の関係もない場合、そのような最適化は許可されます。例えば

struct Base1 { int i; };  struct Base2 { int i; };
void foo(Base1 & b1_ref, Base2 &b2_ref);

これらは異なるオブジェクトを指していると見なすことができるため、コンパイラは特定の仮定を行うことができます。 b2_ref.i=5;を変更できないb1_ref.iため、コンパイラはいくつかの仮定を行うことができます。(実際には、舞台裏で変更を行っている他のスレッド、あるいは POSIX シグナルが存在する可能性もあります。スレッドについて明確にするつもりはないことを認めなければなりません!)

そのため、コンパイラが最適化のために行うことが許可されている仮定があります。このことを考慮:

Base b; // a global variable
void foo() {
    Derived &d_ref = some_function();
    int x1 = b.i;
    d_ref.i = 5;
    int x2 = b.i;
}

これにより、オプティマイザは の動的タイプを認識します。bこれはBase. を 2 回連続して呼び出すとb.i、(他のスレッドなどを除いて) 同じ値が返されるため、コンパイラは後者を に最適化できint x2 = x1ます。some_functionが返された場合Base&、つまりBase &d_ref = some_function();、コンパイラはそのような最適化を行うことができません。

したがって、動的型が であることをコンパイラが認識しているオブジェクトBaseと、派生型 への参照が与えられた場合、Derived&コンパイラは、それらが異なるオブジェクトを参照していると想定する権利があります。コンパイラは、2 つのオブジェクトが相互に参照しないと仮定して、コードを少し書き直すことができます。これにより、少なくとも予測不能な動作が発生する可能性があります。そして、オプティマイザーが許可されている仮定に違反することは、未定義の動作です。

于 2013-11-02T15:40:28.553 に答える