ヌルポインタを介してメンバー関数を呼び出すと、未定義の動作が発生するのはいつですか? 質問する

ヌルポインタを介してメンバー関数を呼び出すと、未定義の動作が発生するのはいつですか? 質問する

次のコードを考えてみましょう。

#include <iostream>

struct foo
{
    // (a):
    void bar() { std::cout << "gman was here" << std::endl; }

    // (b):
    void baz() { x = 5; }

    int x;
};

int main()
{
    foo* f = 0;

    f->bar(); // (a)
    f->baz(); // (b)
}

ヌル ポインターに(b)対応するメンバーがないため、クラッシュすることが予想されます。実際には、ポインターが使用されることはないため、クラッシュしません。x(a)this

はポインター ( )(b)を逆参照し、 null であるため、null の逆参照は常に未定義の動作であると言われているため、プログラムは未定義の動作になります。this(*this).x = 5;this

(a)は未定義の動作になりますか? 関数 (およびx) が両方とも静的である場合はどうなりますか?

ベストアンサー1

どちらも未定義の動作になります。メンバー関数をヌル ポインター経由で呼び出すと(a)(b)常に未定義の動作になります。関数が静的である場合も技術的には未定義ですが、これには異論があります。


最初に理解すべきことは、ヌル ポインターを逆参照することが未定義の動作である理由です。C++03 では、実際にはここで少し曖昧さがあります。

それでも「ヌルポインターを逆参照すると未定義の動作が発生します」§1.9/4 と §8.3.2/4 の両方の注記で言及されていますが、明示的に述べられていることはありません。(注記は非規範的です。)

しかし、§3.10/2からそれを推測することはできます。

lvalue はオブジェクトまたは関数を参照します。

逆参照すると、結果は左辺値になります。ヌルポインタではないオブジェクトを参照するため、lvalue を使用すると未定義の動作が発生します。問題は、前の文がまったく述べられていないことです。lvalue を「使用する」とはどういう意味でしょうか。単に生成するだけでしょうか、それとも lvalue から rvalue への変換を実行するというより正式な意味でそれを使用するのでしょうか。

いずれにせよ、右辺値に変換することはできません (§4.1/1)。

lvalue が参照するオブジェクトが T 型のオブジェクトではなく、T から派生した型のオブジェクトでもない場合、またはオブジェクトが初期化されていない場合、この変換を必要とするプログラムの動作は未定義になります。

ここでは明らかに未定義の動作です。

曖昧さは、それが服従に対する未定義の行動であるかどうかから生じます使用しない無効なポインタから値を取得する(つまり、左辺値を取得するが、右辺値に変換しない)。そうでない場合は、はint *i = 0; *i; &(*i);明確に定義されています。これはアクティブな問題

したがって、厳密な「ヌル ポインターを逆参照すると、未定義の動作が発生する」という見解と、弱い「逆参照されたヌル ポインターを使用すると、未定義の動作が発生する」という見解があります。

さて、その質問について考えてみましょう。


はい、(a)未定義の動作になります。実際、thisがnullの場合、関数の内容に関係なく結果は未定義です。

これは§5.2.5/3から導かれる:

E1が「クラスXへのポインタ」型である場合、式はE1->E2同等の形式に変換されます。(*(E1)).E2;

*(E1)厳密な解釈では未定義の動作になり、.E2それを右辺値に変換して、弱い解釈では未定義の動作にします。

また、これは (§9.3.1/1) から直接的に未定義の動作であることがわかります。

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


静的関数の場合、厳密な解釈と弱い解釈によって違いが生じます。厳密に言えば、未定義です。

静的メンバーは、クラス メンバー アクセス構文を使用して参照できます。その場合、オブジェクト式が評価されます。

つまり、非静的であるかのように評価され、 で再び null ポインターを逆参照します(*(E1)).E2

ただし、E1は静的メンバー関数呼び出しでは使用されていないため、弱い解釈を使用すると呼び出しは適切に定義されます。 は*(E1)lvalue になり、静的関数が解決され、*(E1)が破棄されて、関数が呼び出されます。 lvalue から rvalue への変換は行われないため、未定義の動作はありません。

C++0x では、n3126 時点ではあいまいさが残っています。今のところは安全のために、厳密な解釈を使用してください。

おすすめ記事