web-dev-qa-db-ja.com

ステートメント `int val =(++ i> ++ j)? ++ i:++ j; `未定義の動作を呼び出しますか?

次のプログラムがあるとします。

#include <stdio.h>
int main(void)
{
    int i = 1, j = 2;
    int val = (++i > ++j) ? ++i : ++j;
    printf("%d\n", val); // prints 4
    return 0;
}

valの初期化は、いくつかの未定義の動作を隠す可能性があるようですが、オブジェクトが複数回変更されたり、途中でシーケンスポイントなしで変更されて使用されたりするポイントは見当たりません。誰かがこれを修正するか、私を裏付けることができますか?

24
max1000001

このコードの動作は明確に定義されています。

条件式の最初の式は、2番目の式または3番目の式の前に評価されることが保証されており、2番目または3番目のどちらか一方のみが評価されます。これは C標準 のセクション6.5.15p4で説明されています:

最初のオペランドが評価されます。その評価と2番目または3番目のオペランド(評価される方)の評価の間にシーケンスポイントがあります。 2番目のオペランドは、最初のオペランドが0と等しくない場合にのみ評価されます。 3番目のオペランドは、最初のオペランドが0と等しい場合にのみ評価されます。結果は、2番目または3番目のオペランド(評価される方)の値であり、以下で説明するタイプに変換されます。

式の場合:

int val = (++i > ++j) ? ++i : ++j;

++i > ++jが最初に評価されます。 ijのインクリメントされた値が比較に使用されるため、2 > 3。結果はfalseなので、++jが評価され、++i ではありません。したがって、(再度)j(つまり4)の増分値がvalに割り当てられます。

37
dbush

手遅れですが、多分役に立つでしょう。

_(++i > ++j) ? ++i : ++j;
_

ドキュメント ISO/IEC 9899:201xAnnex C(informative)Sequence points で、シーケンスポイントがあることがわかります

条件付き?:演算子の最初のオペランドの評価と、2番目と3番目のオペランドのどちらが評価されるか

明確に定義された動作を実現するには、2つのシーケンスポイント間で同じオブジェクトを2回(副作用を介して)変更してはなりません。

式で発生する可能性がある唯一の競合は、最初と2番目の_++i_または_++j_の間です。

すべてのシーケンスポイントで、オブジェクトに最後に格納された値は、抽象マシンによって規定された値と一致します(これは、チューリングマシンのように、紙の上で計算するものです)。

_5.1.2.3p3 Program execution_からの引用

式AとBの評価の間にシーケンスポイントが存在するということは、Aに関連付けられているすべての値の計算と副作用が、Bに関連付けられているすべての値の計算と副作用の前にシーケンスされることを意味します。

コードに副作用がある場合、それらは異なる式によって順序付けられます。このルールでは、2つのシーケンスポイント間で、これらの式を好きなように並べ替えることができます。

例えば。 _i = i++_。この式に含まれる演算子はシーケンスポイントを表していないため、必要に応じて副作用である式を並べ替えることができます。 C言語では、これらのシーケンスのいずれかを使用できます

_i = i; i = i+1;_または_i = i+1; i=i;_または_tmp=i; i = i+1 ; i = tmp;_または_tmp=i; i = tmp; i = i+1;_または 計算の抽象的なセマンティクス と同じ結果を提供するものは、この計算の解釈を要求します。標準ISO9899では、C言語を抽象的なセマンティクスとして定義しています。

9
alinsoar

プログラムにUBがない可能性がありますが、質問:ステートメントint val = (++i > ++j) ? ++i : ++j;は未定義の動作を呼び出しますか?

答えはイエスです。 ijは署名されているため、すべてのベットがオフになっているため、インクリメント操作のいずれかまたは両方がオーバーフローする可能性があります。

もちろん、値を小さい整数として指定したため、これは完全な例では発生しません。

5
Doug Currie

回答として技術的に正しいですが、符号付き整数オーバーフローはあまりにも多くの情報を取得したものであるという@Doug Currieについてコメントします。それどころか!

考え直してみると、Dougの答えは正しいだけでなく、例のように完全ではない3行(ただし、ループなどのプログラム)は明確で明確な「はい」に拡張する必要があると想定しています。理由は次のとおりです。

コンパイラーはint i = 1, j = 2;を認識するため、++ iがjと等しくなるため、jまたは++j。現代のオプティマイザはそのような些細なことを理解しています。

もちろん、それらのいずれかがオーバーフローしない限り。しかし、オプティマイザはこれがUBであることを認識しているため、に基づいて最適化すると想定し、決して発生しない

したがって、3項演算子の条件は常にfalseです(この簡単な例では確かですが、ループ内で繰り返し呼び出されたとしても、これは当てはまります!)、iはインクリメントされるだけですoncejは常にインクリメントされますtwice。したがって、jは常にiよりも大きいだけでなく、オーバーフローが発生するまで、反復ごとに増加しますが、これは発生しません発生しません私たちの仮定による)。

したがって、オプティマイザはこれを無条件に++i; j += 2;に変換することができますが、これは確かに期待どおりのものではありません。

同じことが、例えばユーザー提供の入力など、ijunknown値を持つループ。オプティマイザは、操作のシーケンスがijの初期値にのみ依存することを非常によく認識する場合があります。したがって、条件付きの移動が続くインクリメントのシーケンスは、ループを各ケースに1回ずつ複製し、単一のif(i>j)で2つを切り替えることによって最適化できます。そして、その間に、2ずつ繰り返されるループを(j-i)<<1のようなものに折りたたみ、追加するだけかもしれません。か何か。
オーバーフローは発生しないという仮定の下で-これは、オプティマイザが作成を許可されているという仮定であり、doesmake-suchプログラムの全体的な感覚と動作モードを完全に変更する可能性のある修正は完全に問題ありません。

それを試してデバッグしてください。

0
Damon