54

fC99 では、関数とg干渉の副作用があったとしても、式f() + g()にシーケンス ポイントが含まれておらず、シーケンス ポイントが含まれている場合でも、その動作は未規定であるfg考えていました: f() が前に呼び出されるg()、または f() の前に g()。

私はもはや確信が持てません。コンパイラが関数をインライン化し (関数が宣言されていなくてもコンパイラが実行することを決定する場合がありますinline)、命令を並べ替えたらどうなるでしょうか? 上記の2つとは異なる結果が得られるでしょうか? つまり、これは未定義の動作ですか?

これは、私がこのようなことを書こうとしているからではなく、静的アナライザーでそのようなステートメントに最適なラベルを選択するためです。

4

3 に答える 3

25

f() + g()には、少なくとも4つのシーケンスポイントが含まれています。呼び出しの前に1つf()(引数のすべてのゼロが評価された後)。呼び出しの前に1つg()(引数のすべてのゼロが評価された後)。f()1つはリターンの呼び出しです。g()1つはリターンの呼び出しです。さらに、に関連付けられた2つのシーケンスポイントは、に関連付けられた2つのシーケンスポイントf()の前または後の両方で発生しますg()。わかりませんが、シーケンスポイントが発生する順序は、fポイントがgポイントの前に発生するのか、またはその逆であるのかです。

コンパイラがコードをインライン化した場合でも、「あたかも」ルールに従う必要があります。コードは、関数がインターリーブされていない場合と同じように動作する必要があります。これにより、損傷の範囲が制限されます(バグのないコンパイラーを想定)。

したがって、f()g()が評価される順序は指定されていません。しかし、他のすべてはかなりきれいです。


コメントで、supercatは尋ねます:

ソースコード内の関数呼び出しは、コンパイラが独自にインライン化することを決定した場合でも、シーケンスポイントとして残ると思います。それは「インライン」と宣言された関数にも当てはまりますか、それともコンパイラーは余分な自由度を取得しますか?

「あたかも」ルールが適用され、コンパイラは明示的にinline関数を使用するため、シーケンスポイントを省略するための余分な自由度は得られないと思います。(標準で正確な表現を探すのが面倒である)と考える主な理由は、コンパイラがその規則に従って関数をインライン化またはインライン化できないことを許可されているが、プログラムの動作は変更されるべきではないということです(パフォーマンス)。

また、のシーケンスについて何が言えます(a(),b()) + (c(),d())か?c()および/d()またはとの間a()で実行することb()、またはのためにa()またはとのb()c()で実行することは可能d()ですか?

  • 明らかに、aはbの前に実行され、cはdの前に実行されます。cとdがaとbの間で実行される可能性はあると思いますが、コンパイラがそのようなコードを生成する可能性はほとんどありません。同様に、aとbはcとdの間で実行できます。そして、私は「cとd」で「と」を使用しましたが、それは「または」である可能性があります。つまり、これらの操作シーケンスのいずれかが制約を満たしています。

    • 絶対に許可されます
    • あいうえお
    • cdab
    • 許可される可能性があります(a≺b、c≺dの順序を保持します)
    • acbd
    • acdb
    • cadb
    • cabd

     
    私はそれがすべての可能なシーケンスをカバーしていると信じています。Jonathan LefflerとAnArrayOfFunctionsの間のチャットも参照してください。要点は、AnArrayOfFunctionsは「おそらく許可された」シーケンスがまったく許可されていないと考えているということです。

そのようなことが可能であれば、それはインライン関数とマクロの間に大きな違いがあることを意味します。

インライン関数とマクロには大きな違いがありますが、式の順序はその1つではないと思います。つまり、関数a、b、c、またはdのいずれかをマクロに置き換えることができ、マクロ本体の同じシーケンスが発生する可能性があります。主な違いは、インライン関数では、メインの回答で概説されているように、関数呼び出しとコンマ演算子でシーケンスポイントが保証されていることです。マクロを使用すると、関数関連のシーケンスポイントが失われます。(つまり、それは大きな違いかもしれません...)しかし、多くの点で、問題はピンの頭で何人の天使が踊れるかについての質問のようなものです。実際にはそれほど重要ではありません。誰かが私に表現を提示した場合(a(),b()) + (c(),d())コードレビューでは、明確にするためにコードを書き直すように指示します。

a();
c();
x = b() + d();

b()そして、それはvsに重要なシーケンス要件がないことを前提としていますd()

于 2010-10-16T23:02:05.267 に答える
14

シーケンス ポイントのリストについては、付録 C を参照してください。関数呼び出し (すべての引数が評価されてから実行が関数に渡されるまでのポイント) は、シーケンス ポイントです。あなたが言ったように、どちらの関数が最初に呼び出されるかは指定されていませんが、2 つの関数のそれぞれは、他のすべての副作用を見るか、まったく見ないかのいずれかです。

于 2010-10-16T22:28:42.297 に答える
1

@dmckee

まあ、それはコメント内には収まりませんが、ここにあります:

まず、正しい静的アナライザーを作成します。このコンテキストでの「正しい」とは、分析されたコードに疑わしい点がある場合に黙っていないことを意味するため、この段階では、未定義および未指定の動作を楽しく混同します。それらは両方とも重要なコードでは悪く、受け入れられません。当然のことながら、両方について警告します。

しかし、1 つのバグの可能性について警告するのは 1 回だけであり、また、アナライザーが他の (正しくない可能性がある) アナライザーと比較した場合に、ベンチマークで "精度" と "再現率" の観点から判断されることがわかっているため、 1 つの同じ問題について 2 回警告する... それが真のアラームか誤報か (どちらかはわかりません。どちらかはわかりません。そうしないと簡単すぎます)。

したがって、単一の警告を発行したい

*p = x;
y = *p;

は最初のステートメントで有効なポインターpであるため、2 番目のステートメントでも有効なポインターであると見なすことができます。これを推測しないと、精度指標のスコアが下がります。

したがって、上記のコードで最初に警告するとすぐにそれが有効なポインターであると仮定するようにアナライザーに教えてp、2 回目に警告しないようにします。より一般的には、すでに警告したことに対応する値 (および実行パス) を無視することを学びます。

次に、重要なコードを書いている人があまりいないことに気付き、最初の正しい分析の結果に基づいて、残りのコードについて別の軽量な分析を行います。たとえば、C プログラムのスライサーです。

そして、「彼ら」に伝えます。最初の分析によって発せられたすべての (おそらく、多くの場合、誤った) アラームを確認する必要はありません。スライスされたプログラムは、いずれもトリガーされない限り、元のプログラムと同じように動作します。スライサーは、「定義された」実行パスのスライス基準と同等のプログラムを生成します。

そして、ユーザーは喜んでアラームを無視し、スライサーを使用します。

そして、おそらく誤解があることに気づきます。たとえば、ほとんどの実装memmove(オーバーラップ ブロックを処理するもの) は、実際には、同じブロックを指していないポインターで呼び出されると、未指定の動作を呼び出します (同じブロックを指していないアドレスを比較します)。また、アナライザーは両方の実行パスを無視します。どちらも指定されていないためですが、実際には両方の実行パスは同等であり、すべて問題ありません。

したがって、アラームの意味について誤解があってはなりません。アラームを無視するつもりなら、紛れもない未定義の動作だけを除外する必要があります。

そして、これが、未指定の動作と未定義の動作を区別することに強い関心を持つようになる方法です。後者を無視したとしても、誰もあなたを責めることはできません。しかし、プログラマーは前者は何も考えずに書いてしまいますし、あなたのスライサーがプログラムの「間違った振る舞い」を排除していると言っても、彼らは気にしないでしょう。

そして、これは間違いなくコメントに収まらない話の終わりです. ここまで読んでくださった方、申し訳ありません。

于 2010-10-17T03:21:04.713 に答える