3

次の例を検討してください。

class Base {
public:
    int data_;
};

class Derived : public Base {
public:
    void fun() { ::std::cout << "Hi, I'm " << this << ::std::endl; }
};

int main() {
    Base base;
    Derived *derived = static_cast<Derived*>(&base); // Undefined behavior!

    derived->fun(); 

    return 0;
}

C++ 標準によると、関数呼び出しは明らかに未定義の動作です。しかし、利用可能なすべてのマシンとコンパイラ (VC2005/2008、RH Linux および SunOS の gcc) では、期待どおりに動作します ("Hi!" が出力されます)。このコードが正しく動作しない構成を知っている人はいますか? それとも、同じ考えを持つより複雑な例かもしれません (とにかく、Derived は追加のデータを運ぶべきではないことに注意してください)?

アップデート:

標準 5.2.9/8 から:

タイプ「cv1 B へのポインター」の右辺値 (B はクラス タイプ) は、タイプ「cv2 D へのポインター」の右辺値に変換できます。ここで、D は B から派生したクラス (第 10 節) です。 「D へのポインター」から「B へのポインター」への変換が存在し (4.10)、cv2 が cv1 と同じ cvqualification であるか、cv1 より大きい cvqualification であり、B が D の仮想基底クラスではない。null ポインター値 (4.10)変換先の型の null ポインター値に変換されます。型「cv1 B へのポインター」の右辺値が実際には型 D のオブジェクトのサブオブジェクトである B を指している場合、結果のポインターは型 D の外側のオブジェクトを指します。それ以外の場合、キャストの結果は未定義です。

そしてもう1つ9.3.1(@Agent_Lに感謝):

クラス X の非静的メンバー関数が、型 X または X から派生した型ではないオブジェクトに対して呼び出された場合、動作は未定義です。

ありがとう、マイク。

4

6 に答える 6

9

この関数fun()は、ポインタが何であるかに関係なく実際には何もしませthisん。仮想関数ではないため、関数を検索するために特別なことは何も必要ありません。基本的に、通常の (非メンバー) 関数と同じように呼び出されますが、thisポインターが正しくありません。クラッシュしません。これは、完全に有効な未定義の動作です (矛盾していない場合)。

于 2012-04-23T09:51:19.933 に答える
5

コードへのコメントが正しくありません。

Derived *derived = static_cast<Derived*>(&base);
derived->fun(); // Undefined behavior!

修正版:

Derived *derived = static_cast<Derived*>(&base);  // Undefined behavior!
derived->fun(); // Uses result of undefined behavior

未定義の動作はstatic_cast. この悪質なポインターのその後の使用も、未定義の動作です。未定義の動作は、コンパイラ ベンダーにとって脱獄のカードです。コンパイラによるほとんどすべての応答は、標準に準拠しています。

コンパイラがキャストを拒否するのを止めるものは何もありません。優れたコンパイラは、そのために致命的なコンパイル エラーを発行する可能性がありますstatic_castこの場合、違反は簡単にわかります。一般に、これを確認するのは簡単ではないため、ほとんどのコンパイラはわざわざチェックしません。

ほとんどのコンパイラは代わりに最も簡単な方法をとります。この場合、簡単な方法は、 class のインスタンスBaseへのポインタが class のインスタンスへのポインタであると単純に装うことですDerived。あなたの関数はかなり無害なので、この場合Derived::fun()の簡単な方法はかなり無害な結果をもたらします。

良性の良い結果が得られたからといって、すべてがうまくいっているわけではありません。まだ未定義の動作です。最善の策は、未定義の動作に決して依存しないことです。

于 2012-04-23T11:37:27.017 に答える
3

同じマシンで同じコードを無限に実行すると、運が良ければ、正しく動作せず、予想外に動作することがあります。

理解しておくべきことは、未定義の動作 (UB) は、期待どおりに実行されないという意味ではないということです。1 回、2 回、10 回、さらには無限に実行される可能性があります。UB は単に、期待どおりに動作することが保証されていないことを意味します。

于 2012-04-23T09:51:14.910 に答える
1

コードが何をしているのかを理解する必要があります。「this」は、コンパイラによって生成される隠しポインタです。

class Base
{
public:
    int data_;
};

class Derived : public Base
{

};


void fun(Derived* pThis) 
{
::std::cout << "Hi, I'm " << pThis << ::std::endl; 
}

//because you're JUST getting numerical value of a pointer, it can be same as:
void fun(void* pThis) 
{
    ::std::cout << "Hi, I'm " << pThis << ::std::endl; 
}

//but hey, even this is still same:
void fun(unsigned int pThis) 
{
    ::std::cout << "Hi, I'm " << pThis << ::std::endl; 
}

これで明らかです。この関数は失敗することはありません。NULL またはその他のまったく関係のないクラスを渡すこともできます。 動作は未定義ですが、ここで問題が発生することはありません。

//編集: OK、標準によれば、状況は等しくありません。((Derived*)NULL)->fun(); UB を明示的に宣言します。ただし、この動作は通常、呼び出し規則に関するコンパイラ ドキュメントで定義されています。「私が知っているすべてのコンパイラについて、何も問題はありません」と書くべきでした。

于 2012-04-23T10:38:38.880 に答える
1

このコードが頻繁に機能する実際的な理由は、これを破るものはすべて、リリース/パフォーマンス最適化ビルドで最適化される傾向があるためです。ただし、エラーの検出に重点を置いたコンパイラ設定 (デバッグ ビルドなど) は、これでトリップする可能性が高くなります。

そのような場合、あなたの仮定(「Derivedとにかく追加のデータを運ぶべきではないことに注意してください」)は成り立ちません。デバッグを容易にするために、それは間違いなくすべきです。

もう少し複雑な例は、さらにトリッキーです。

class Base {
public:
    int data_;
    virtual void bar() { std::cout << "Base\n"; }
};

class Derived : public Base {
public:
    void fun() { ::std::cout << "Hi, I'm " << this << ::std::endl; }
    virtual void bar() { std::cout << "Derived\n"; }
};

int main() {
    Base base;
    Derived *derived = static_cast<Derived*>(&base); // Undefined behavior!

    derived->fun(); 
    derived->bar();
}

合理的なコンパイラは、vtable をスキップして静的に呼び出すことを決定する場合があります。Base::bar()これは、呼び出しているオブジェクトであるためですbar()。または、呼び出したので がderived実数を指す必要があると判断し、vtable をスキップして を呼び出す場合もあります。ご覧のとおり、状況を考えると、どちらの最適化も非常に合理的です。DerivedfunDerived::bar()

これで、Undefined Behavior がそれほど驚くべきものである理由がわかります。コンパイラは、ステートメント自体が正しくコンパイルされていても、UB を使用したコードに従って誤った仮定を行う可能性があります。

于 2012-04-23T14:06:21.943 に答える
1

たとえば、コンパイラはコードを最適化することがあります。わずかに異なるプログラムを考えてみましょう:

if(some_very_complex_condition)
{
  // here is your original snippet:

  Base base;
  Derived *derived = static_cast<Derived*>(&base); // Undefined behavior!

  derived->fun(); 
}

コンパイラは

(1) 未定義の動作を検出する

(2) プログラムが未定義の動作を公開すべきではないと仮定する

したがって (コンパイラが判断します) _some_very_complex_condition_ は常に false にする必要があります。これを想定すると、コンパイラはコード全体を到達不能として除外する場合があります。

[編集]コンパイラが UB ケースを「処理する」コードをどのように削除するかという実際の例:

GCC を使用した x86 で整数オーバーフローが無限ループを引き起こすのはなぜですか?

于 2012-04-23T13:37:13.690 に答える