web-dev-qa-db-ja.com

なぜa =(a + b)-(b = a)は2つの整数を交換するのに悪い選択なのですか?

一時変数を使用したり、ビットごとの演算子を使用したりせずに2つの整数を交換するために、このコードに出くわしました。

_int main(){

    int a=2,b=3;
    printf("a=%d,b=%d",a,b);
    a=(a+b)-(b=a);
    printf("\na=%d,b=%d",a,b);
    return 0;
}
_

しかし、このコードには、評価の順序を決定するためのシーケンスポイントが含まれていないため、スワップステートメントa = (a+b) - (b=a);で未定義の動作があると思います。

私の質問は次のとおりです:これは2つの整数を交換するための受け入れ可能な解決策ですか?

58
ashfaque

いいえ、これは受け入れられません。このコードは Undefined behavior を呼び出します。これは、bに対する操作が定義されていないためです。式で

a=(a+b)-(b=a);  

bが最初に変更されるのか、その値が式(a+bシーケンスポイント がないため。
標準のsyasをご覧ください:

C11:6.5式:

同じスカラーオブジェクトの異なる副作用または同じスカラーオブジェクトの値を使用した値計算、動作は未定義です。 式の部分式の許容可能な順序が複数ある場合、そのようなシーケンスされていない副作用がいずれかの順序で発生すると、動作は未定義になります。84)1

シーケンスポイントと未定義の動作の詳細については、 C-faq- 3.8answer を参照してください。


1.強調は私のものです。

83
haccks

私の質問は-これは2つの整数を交換するための許容できる解決策ですか?

誰に受け入れられる?それが私に受け入れられるかどうかを尋ねている場合、私が行っていたコードレビューを通過することはないでしょう、私を信じてください。

なぜa =(a + b)-(b = a)は2つの整数を交換するのに悪い選択なのですか?

以下の理由により:

1)お気づきのように、Cでは実際にそれが行われるという保証はありません。それは何でもできます。

2)議論のために、C#と同様に、2つの整数を実際に交換するとします。 (C#は、副作用が左から右に発生することを保証します。)コードの意味が完全に明らかでないため、コードは依然として受け入れられません。コードは巧妙なトリックの束であってはなりません。それを読んで理解しなければならないあなたの後に来る人のためのコードを書いてください。

3)再び、それが機能すると仮定します。これは単なる誤りであるため、コードはまだ受け入れられません。

Temporary変数を使用したり、ビット単位の演算子を使用したりせずに2つの整数を交換するために、このコードに出くわしました。

それは単に誤りです。このトリックでは、一時変数を使用してa+bの計算を保存します。変数はコンパイラに代わって生成され、名前は付けられませんが、そこにあります。目標が一時的なものを排除することである場合、これはそれを悪化させ、より良くはしません!そして、なぜ最初に一時的なものを排除したいのですか?安い!

4)これは整数に対してのみ機能します。整数以外の多くのものを交換する必要があります。

要するに、実際に事態を悪化させる巧妙なトリックを考え出そうとするのではなく、明らかに正しいコードの記述に集中して時間を費やしてください。

48
Eric Lippert

a=(a+b)-(b=a)には少なくとも2つの問題があります。

あなたがあなた自身に言及した1つ:シーケンスポイントの欠如は、動作が定義されていないことを意味します。そのため、何でも起こり得ることです。たとえば、どちらが最初に評価されるかは保証されません:a+bまたはb=a。コンパイラーは、割り当て用のコードを最初に生成するか、まったく異なることを行うかを選択できます。

もう1つの問題は、符号付き演算のオーバーフローが未定義の動作であることです。 a+bがオーバーフローした場合、結果は保証されません。例外さえ投げられるかもしれません。

32
Joni

未定義の動作とスタイルに関する他の回答とは別に、一時変数を使用するだけの単純なコードを記述した場合、コンパイラーは値をトレースし、生成されたコードで実際に値をスワップせず、スワップされた値を後で使用する可能性がありますケース。それはあなたのコードでそれを行うことはできません。コンパイラーは通常、マイクロ最適化であなたよりも優れています。

そのため、コードが遅くなり、理解が難しくなり、おそらく信頼できない未定義の動作も発生する可能性があります。

29
jcoder

Gccと-Wallを使用する場合、コンパイラーはすでに警告を出します

a.c:3:26:警告:「b」での操作は未定義の可能性があります[-Wsequence-point]

このような構造を使用するかどうかは、パフォーマンスの点からも議論の余地があります。あなたが見るとき

void swap1(int *a, int *b)
{
    *a = (*a + *b) - (*b = *a);
}

void swap2(int *a, int *b)
{
    int t = *a;
    *a = *b;
    *b = t;
}

アセンブリコードを調べて

swap1:
.LFB0:
    .cfi_startproc
    movl    (%rdi), %edx
    movl    (%rsi), %eax
    movl    %edx, (%rsi)
    movl    %eax, (%rdi)
    ret
    .cfi_endproc

swap2:
.LFB1:
    .cfi_startproc
    movl    (%rdi), %eax
    movl    (%rsi), %edx
    movl    %edx, (%rdi)
    movl    %eax, (%rsi)
    ret
    .cfi_endproc

コードを難読化してもメリットはありません。


C++(g ++)コードを見ると、基本的に同じですが、moveが考慮されます

#include <algorithm>

void swap3(int *a, int *b)
{
    std::swap(*a, *b);
}

同一のアセンブリ出力を提供します

_Z5swap3PiS_:
.LFB417:
    .cfi_startproc
    movl    (%rdi), %eax
    movl    (%rsi), %edx
    movl    %edx, (%rdi)
    movl    %eax, (%rsi)
    ret
    .cfi_endproc

Gccの警告を考慮に入れても、技術的な改善は見られないので、標準的な手法に固執します。これがボトルネックになった場合でも、この小さなコードを改善または回避する方法を調査できます。

28
Olaf Dietsche

ステートメント:

a=(a+b)-(b=a);

未定義の動作を呼び出します。引用された段落の2番目は違反します。

(C99、6.5p2)「前と次のシーケンスポイントの間で、オブジェクトは、式の評価によって最大で一度、変更された格納された値を持つものとします。さらに、以前の値は、保存されます。 "

8
ouah

201 にまったく同じ例で質問が投稿されました。

a = (a+b) - (b=a);

Steve Jessop は警告します:

ちなみに、そのコードの動作は未定義です。 aとbの両方が、シーケンスポイントを介さずに読み書きされます。手始めに、コンパイラは、a + bを評価する前にb = aを評価する権利内に十分にあります。

2012 に投稿された質問からの説明です。かっこがないため、サンプルは正確には同じではないが同じですが、それでも答えは適切です。

C++では、算術式の部分式に時間的な順序はありません。

a = x + y;

Xが最初に評価されますか、それともyですか?コンパイラはどちらかを選択するか、まったく異なるものを選択できます。評価の順序は、演算子の優先順位と同じではありません。演算子の優先順位は厳密に定義されており、 評価の順序は、プログラムにシーケンスポイントがある粒度にのみ定義されます。

実際、VLIWアーキテクチャなど、一部のアーキテクチャでは、xとyの両方を同時に評価するコードを生成することが可能です。

ここで、N1570からのC11標準見積もりについて:

付録J.1/1

次の場合の不特定の動作です。

—関数呼び出し()&&||? :、およびカンマ演算子(6.5)。

—代入演算子のオペランドが評価される順序(6.5.16)。

附属書J.2/1

次の場合、これは未定義の動作です。

—スカラーオブジェクトの副作用は、同じスカラーオブジェクトの異なる副作用、または同じスカラーオブジェクトの値を使用した値の計算(6.5)に比べて、順序付けされていません。

6.5/1

式は、値の計算を指定したり、オブジェクトや関数を指定したり、副作用を生成したり、それらの組み合わせを実行したりする一連の演算子とオペランドです。演算子のオペランドの値計算は、演算子の結果の値計算の前にシーケンスされます。

6.5/2

スカラーオブジェクトの副作用が同じスカラーオブジェクトの異なる副作用または同じスカラーオブジェクトの値を使用した値の計算に関連してシーケンスされていない場合、動作は未定義です。式の部分式の許容可能な順序が複数ある場合、そのようなシーケンスされていない副作用がいずれかの順序で発生すると、動作は未定義になります。84)

6.5/3

演算子とオペランドのグループ化は構文で示されます。85)後で指定されている場合を除き、副次式の副作用と値の計算はシーケンスされていません。86)

未定義の動作に依存しないでください。

いくつかの代替案:C++では次を使用できます

  std::swap(a, b);

XORスワップ:

  a = a^b;
  b = a^b;
  a = a^b;
8
user1508519

問題は、C++標準によると

特に明記されていない限り、個々の演算子のオペランドおよび個々の式の部分式の評価は、シーケンスされていません。

だからこの表現

a=(a+b)-(b=a);

未定義の動作があります。

5

XORスワップアルゴリズム を使用してオーバーフローの問題を回避し、ワンライナーを使用できます。

ただし、_c++_タグがあるため、読みやすくするために単純なstd::swap(a, b)を使用することをお勧めします。

5
dreamzor

他のユーザーによって引き起こされた問題の他に、このタイプのスワップには別の問題があります:ドメイン

a+bが制限を超えた場合はどうなりますか? 0から100までの数値を処理するとします。 80+ 60は範囲外です。

4
catalinux