57

一時変数やビットごとの演算子を使用せずに 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);が含まれていないため、swap ステートメントで未定義の動作があると思います。

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

4

10 に答える 10

82

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

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

シーケンスポイントがないため、b最初に変更されるか、その値が式 () で使用されるかは定かではありません。 どのような標準システムを参照してください:a+b

C11: 6.5 式:

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

シーケンス ポイントと未定義の動作の詳細な説明については、C-faq-3.8とこの回答をお読みください。


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

于 2013-12-27T12:29:58.810 に答える
48

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

誰に受け入れられますか?それが私に受け入れられるかどうかを尋ねている場合、それは私が参加したコードレビューを通過することはありません.私を信じてください.

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

次の理由によります。

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

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

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

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

それは単に間違っています。このトリックは、一時変数を使用して の計算を格納しa+bます。変数は、ユーザーに代わってコンパイラによって生成され、名前は付けられませんが、そこにあります。目標が一時的なものを排除することである場合、これは事態を悪化させます。そもそも、なぜ一時的なものを排除したいのでしょうか? 彼らは安いです!

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

要するに、実際に事態を悪化させる巧妙なトリックを考え出すよりも、明らかに正しいコードを書くことに集中して時間を費やしてください。

于 2013-12-27T17:59:06.100 に答える
32

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

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

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

于 2013-12-27T12:35:48.190 に答える
28

gcc を使用し-Wallていて、コンパイラがすでに警告している場合

ac: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 の警告を考慮に入れても、技術的な利点が見られない場合は、標準的な手法を使用することをお勧めします。これがボトルネックになった場合でも、この小さなコードを改善または回避する方法を調査できます。

于 2013-12-27T16:57:55.850 に答える
8

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

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

Steve Jessopはそれに対して次のように警告しています。

ちなみに、そのコードの動作は未定義です。a と b はどちらも、シーケンス ポイントを介在させずに読み書きされます。まず第一に、コンパイラは a+b を評価する前に b=a を評価する権利を十分に持っています。

これは、 2012 年に投稿された質問からの説明です。括弧がないため、サンプルはまったく同じではないことに注意してください。ただし、それでも回答は適切です。

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

a = x + y;

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

実際、一部のアーキテクチャでは、x と y の両方を同時に評価するコードを発行することができます。たとえば、VLIW アーキテクチャです。

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

演算子とオペランドのグループ化は、構文によって示されます。

未定義の動作に依存するべきではありません。

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

  std::swap(a, b);

XOR スワップ:

  a = a^b;
  b = a^b;
  a = a^b;
于 2013-12-27T18:24:11.860 に答える
5

XOR スワップ アルゴリズムを使用して、オーバーフローの問題を防ぎながらワンライナーを維持できます。

しかし、あなたはタグを持っているので、読みやすくするためにc++単純な方がいいと思います.std::swap(a, b)

于 2013-12-27T12:26:11.553 に答える