web-dev-qa-db-ja.com

nullインスタンスでメンバー関数を呼び出すと、未定義の動作が発生するのはいつですか?

次のコードを検討してください。

#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)
}

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

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

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

116
GManNickG

_(a)_と_(b)_の両方は、未定義の動作になります。 NULLポインターを介してメンバー関数を呼び出すことは、常に未定義の動作です。関数が静的な場合、技術的にも未定義ですが、いくつかの論争があります。


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

"nullポインターを参照解除すると未定義の動作が発生する"が§1.9/ 4と§8.3.2/ 4の両方のメモに記載されていますが、明示的には決してありません。 (注記は非規範的です。)

ただし、§3.10/ 2から推測することができます。

左辺値はオブジェクトまたは関数を指します。

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

とにかく、間違いなく右辺値に変換することはできません(4.1/1):

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

ここでは間違いなく未定義の動作です。

あいまいさは、それが従うべき未定義の動作かどうかに由来しますただし使用しない無効なポインターからの値(つまり、左辺値を取得しますが、右辺値に変換しません)。そうでない場合、int *i = 0; *i; &(*i);は明確に定義されています。これは アクティブな問題 です。

したがって、厳密な「nullポインターの逆参照、未定義の動作の取得」ビューと、弱い「逆参照nullポインターの使用、未定義の動作の取得」ビューがあります。

ここで質問を検討します。


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

これは、§5.2.5/ 3から続きます。

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

*(E1)は厳密な解釈で未定義の動作を引き起こし、_.E2_はそれを右辺値に変換し、弱い解釈では未定義の動作にします。

また、(§9.3.1/ 1)から直接定義されていない動作であることにもなります。

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


静的関数では、厳密な解釈と弱い解釈が違いを生みます。厳密に言えば、未定義です。

静的メンバーは、クラスメンバーアクセス構文を使用して参照できます。この場合、object-expressionが評価されます。

つまり、まるで静的でないかのように評価され、もう一度_(*(E1)).E2_を使用してNULLポインターを逆参照します。

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

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

112
GManNickG

明らかに未定義とは、未定義を意味しますが、予測可能な場合もあります。私が提供しようとしている情報は、確かに保証されているわけではないので、動作するコードに決して頼るべきではありませんが、デバッグ時に役立つかもしれません。

オブジェクトポインターで関数を呼び出すと、ポインターが逆参照され、UBが発生すると考えるかもしれません。実際には、関数が仮想でない場合、コンパイラーは、ポインターを最初のパラメーターthisとして渡すプレーン関数呼び出しに変換し、逆参照をバイパスして時限爆弾を作成します。呼び出されたメンバー関数用。メンバー関数がメンバー変数または仮想関数を参照しない場合、エラーなしで実際に成功する可能性があります。成功は「未定義」の世界に収まることを忘れないでください!

MicrosoftのMFC関数 GetSafeHwnd は、実際にこの動作に依存しています。彼らが何を吸っていたのか分かりません。

仮想関数を呼び出す場合、vtableに到達するためにポインターを逆参照する必要があり、確実にUBを取得します(おそらくクラッシュしますが、保証はないことに注意してください)。

30
Mark Ransom