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


120

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

#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ポインタは使用されないため、クラッシュしません。

ので(b)逆参照thisポインタ((*this).x = 5;)、およびthisnullであるヌルを逆参照することは、常に未定義の動作と言われているように、プログラムは、未定義の動作に入ります。

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


両方の関数が静的である場合、xをbaz内でどのように参照できますか?(xは非静的メンバー変数です)
legends2k

4
@ legends2k:ふりxも静的になりました。:)
GManNickG 2010

確かに、ただし(a)の場合は、すべての場合で同じように機能します。つまり、関数が呼び出されます。ただし、ポインターの値を0から1に(たとえば、reinterpret_castを介して)置き換えると、ほぼ常にクラッシュします。0の値の割り当て、つまりNULLは、ケースaのように、コンパイラーにとって特別なものを表しますか?割り当てられた他の値で常にクラッシュするのはなぜですか?
Siddharth Shankaran

5
興味深い:C ++の次のリビジョンが来ると、ポインターの逆参照はなくなります。現在ならない間接実行ポインタを介し。詳細については、次のリンクから間接参照を実行してください:N3362
James McNellis

3
NULLポインターでメンバー関数を呼び出すことは、常に未定義の動作です。あなたのコードを見るだけで、未定義の動作がゆっくりと私の首を這い上がるのを感じることができます!
fredoverflow 2012

回答:


113

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


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

が、「未定義の動作にヌルポインタ結果を参照解除」 §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がnullの場合、関数内容に関係なく、結果は未定義です。

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

場合E1種類有する「クラスXへのポインタを、」式はE1->E2等価な形式に変換され(*(E1)).E2;

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

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

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


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

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

つまり、静的ではないかのように評価され、もう一度nullポインターをで逆参照します(*(E1)).E2

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

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


5
+1。ペダントリーを続けると、「弱い定義」の下で、非静的メンバー関数は「タイプXでないオブジェクトに対して」呼び出されていません。まったくオブジェクトではない左辺値に対して呼び出されました。したがって、提案されたソリューションは、引用する句に「またはlvalueが空のlvalueの場合」というテキストを追加します。
スティーブジェソップ

少し説明してもらえますか?特に、「クローズされた問題」と「アクティブな問題」のリンクで、問題番号は何ですか?また、これがクローズドな問題である場合、静的関数のはい/いいえの答えは何ですか?私はあなたの答えを理解しようとする最後のステップを逃しているように感じます。
Brooks Moses

4
CWGの欠陥315は、「クローズされた問題」ページに存在するように「クローズされた」とは思いません。その理由は、「左辺値が右辺値に変換されない限り、nullの*pときpはエラーではないから」と認められているためです。ただし、それは「空の左辺値」の概念に依存しています。これは、CWG欠陥232に対する提案された解決策の一部ですが、採用されていません。したがって、C ++ 03とC ++ 0xの両方の言語では、左辺値から右辺値への変換がない場合でも、nullポインターの逆参照は未定義です。
James McNellis、2010年

1
@JamesMcNellis:私の理解でpは、読み取り時に何らかのアクションをトリガーするが宣言されていないハードウェアアドレスの場合volatile、ステートメント*p;は実際にはそのアドレスを読み取る必要はありませんが、許可されます。&(*p);しかし、声明はそうすることを禁じられます。場合*pしたvolatile、読み取りが必要とされるであろう。どちらの場合も、ポインターが無効な場合、最初のステートメントが未定義の動作にならないかどうかはわかりませんが、2番目のステートメントがなぜなのかはわかりません。
スーパーキャット2014年

1
「.E2はそれを右辺値に変換します」-ええと、そうではありません
MM

30

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

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

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

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


1
GetSafeHwndは最初に!thisチェックを実行し、trueの場合はNULLを返します。次に、SEHフレームを開始し、ポインターを逆参照します。メモリアクセス違反(0xc0000005)がある場合、これがキャッチされ、呼び出し元にNULLが返されます。それ以外の場合は、HWNDが返されます。
ПетърПетров

@ПетърПетров私がのコードを見てからかなりの数年が経ちGetSafeHwndましたが、それ以来、彼らがそれを拡張した可能性があります。そして、彼らがコンパイラの仕組みに関する内部知識を持っていることを忘れないでください!
Mark Ransom 2014年

私は同じ効果を持つサンプル可能な実装を記載しています、何それは本当にやるんは、デバッガを使用して:)リバースエンジニアリングする
ПетърПетров

1
「彼らはコンパイラの仕組みに関する内部知識を持っています!」-g ++がWindows APIを呼び出すコードをコンパイルできるようにするMinGWなどのプロジェクトの永遠の問題の原因
MM

@MMこれは不公平だと私たちは皆同意するでしょう。そのため、互換性に関する法律もあり、それを維持することはほんの少し違法になると思います。
v.oddou 2017
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.