126

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

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

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

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

4

2 に答える 2

121

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


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

§1.9 /4と§8.3.2/4の両方の注記で「nullポインタを逆参照すると未定義の動作が発生する」と述べられていますが、明示的には述べられていません。(メモは非規範的です。)

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

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

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

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

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

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

あいまいさは、それが未定義の動作であるかどうかに起因しますが、無効なポインターからの値を使用しません(つまり、左辺値を取得しますが、右辺値に変換しません)。そうでない場合は、int *i = 0; *i; &(*i);明確に定義されています。これは活発な問題です。

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

ここで、質問について検討します。


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

これは§5.2.5/3から続く:

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

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

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

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


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

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

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

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

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

于 2010-03-18T23:21:13.253 に答える
34

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

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

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

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

于 2010-09-29T21:51:49.873 に答える