Почему a = (a+b) - (b=a) плохой выбор для замены двух целых чисел?


я наткнулся на этот код для замены двух целых чисел без использования временной переменной или использовать побитовые операторы.

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;
}

но я думаю, что этот код имеет неопределенное поведение в инструкции swap a = (a+b) - (b=a); так как он не содержит каких-либо точки последовательности для определения порядка оценки.

мой вопрос: это приемлемое решение для замены двух целых чисел?

10 58

10 ответов:

нет. Это неприемлемо. Этот код вызывает неопределенное поведение. Это из-за операции на b Не определен. В выражении

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

не уверен, что b сначала изменяется или его значение используется в выражении (a+b) из-за отсутствия точка последовательности.
Смотрите, какой стандарт syas:

C11: 6.5 Выражения:

если сторона эффект на скалярный объект не имеет последовательности относительно другого побочного эффекта на тот же скалярный объект или вычисление значения с использованием значения того же скаляра объект, поведение не определено. если есть несколько допустимых порядков подвыражения выражения, поведение не определено, если такая непоследовательная сторона эффект возникает в любом из порядков.84)1.

читать C-faq- 3.8 и ответ для более подробного объяснения точки последовательности и неопределенного поведения.


1. Акцент-мой.

мой вопрос - это приемлемое решение для замены двух целых чисел?

приемлемо для кого? Если вы спрашиваете, приемлемо ли это для меня, это не пройдет мимо любого обзора кода, в котором я был, поверьте мне.

почему a=(a+b)-(b=a) плохой выбор для замены двух целых чисел?

по следующим причинам:

1) Как вы заметили, в C нет никакой гарантии, что он действительно это делает. Это могло бы сделать что угодно.

2) Предположим, ради аргумента, что это действительно поменять местами два числа, как это происходит в C#. (В C# гарантирует, что побочные эффекты происходят слева направо.) Код все равно был бы неприемлем, потому что совершенно не очевидно, в чем его смысл! Код не должен быть кучей умных трюков. Напишите код для человека, идущего за вами, который должен прочитать и понять его.

3) опять же, предполагаю, что это работает. Код по-прежнему неприемлем, потому что это просто простая ложь:

я наткнулся на этот код для замены двух целых чисел без использования временной переменной или использование побитовых операторов.

Это просто ложь. Этот трюк использует временную переменную для хранения вычисления a+b. Переменная генерируется компилятором от вашего имени и не имеет имени, но она есть. Если цель состоит в том, чтобы устранить временные, это делает его хуже, а не лучше! И почему вы хотите устранить естественно, в первую очередь? Они дешевые!

4) это работает только для целых чисел. Многие вещи должны быть заменены, кроме целых чисел.

есть по крайней мере две проблемы с a=(a+b)-(b=a).

один вы упоминаете себя: отсутствие точек последовательности означает, что поведение не определено. Таким образом, все что угодно может случиться. Например, нет никакой гарантии того, что оценивается первым:a+b или b=a. Компилятор может сначала создать код для назначения или сделать что-то совершенно другое.

еще одна проблема заключается в том, что переполнение знаковой арифметики не определено поведение. Если a+b переполнения нет никакой гарантии результатов; даже исключение может быть брошен.

помимо других ответов, о неопределенном поведении и стиле, Если вы пишете простой код, который просто использует временную переменную, компилятор, вероятно, может отслеживать значения и фактически не заменять их в сгенерированном коде, а просто использовать замененные значения позже в некоторых случаях. Он не может сделать это с вашим кодом. Компилятор обычно лучше, чем вы при микро-оптимизации.

Так что, скорее всего, ваш код медленнее, труднее понять, и, вероятно, ненадежное неопределенное поведение тоже.

если вы используете 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 и не видя технической выгоды, я бы сказал, придерживайтесь стандартных методов. Если это когда-нибудь станет узким местом, вы все равно можете исследовать, как улучшить или избежать этого небольшого фрагмента кода.

инструкции:

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

вызывает неопределенное поведение. Второе должно в процитированном абзаце нарушаться:

(C99, 6.5p2) "между предыдущей и следующей точками последовательности объект должен иметь сохраненное значение, измененное не более одного раза путем вычисления выражения. кроме того, предыдущее значение должно считываться только для определения значения, подлежащего хранению."

вопрос был отправлен обратно в 2010 С точно таким же примером.

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

Стив Джессоп предостерегает от этого:

поведение этого кода не определено, кстати. Оба a и b читаются и написано без промежуточной точки последовательности. Для начала, компилятор был бы вполне в пределах своих прав, чтобы оценить b=A раньше оценка a+b.

вот объяснение из опубликованного вопроса в 2012. Обратите внимание, что образец не ровно то же из-за отсутствия скобок, но ответ по-прежнему актуален, тем не менее.

в C++ подвыражения в арифметических выражениях не имеют временных заказ.

a = x + y;

сначала оценивается x или y? Компилятор может выбрать либо, либо он может выбрать что-то совершенно другое. Порядок оценки не то же самое, что приоритет оператора: приоритет оператора строго определено, и порядок оценки определяется только для детализации что ваша программа имеет точки последовательности.

на самом деле, на некоторых архитектурах можно выдавать код, который оценивает и x и y одновременно -- например, VLIW зодчие.

теперь для стандартных цитат C11 из N1570:

Приложение J. 1/1

это неопределенное поведение когда:

- заказа в каких подвыражениях оцениваются и в каком порядке стороны эффекты имеют место, за исключением указанного для функции-call (),&&, ||,? :, и операторы запятая (6.5).

- порядок, в котором вычисляются операнды оператора присваивания (6.5.16).

Приложение J. 2 / 1

это неопределенное поведение, когда:

- побочный эффект на скалярный объект не имеет последовательности относительно любого из a другой побочный эффект в тот же скалярный объект или значение, вычисление используя значение того же скалярного объекта (6.5).

6.5/1

выражение-это последовательность операторов и операндов, которая задает вычисление значения, или которое обозначает объект или функцию, или это создает побочные эффекты или выполняет их комбинацию. Вычисления значения операнда оператор секвенирован перед вычислением значения результата работы оператора.

6.5/2

если побочный эффект на скалярный объект непоследовательными по отношению к любой другой побочный эффект в тот же скалярный объект или значение вычисление, используя значение же скалярный объект, поведение не определено. Если существует несколько допустимых порядков подвыражения выражения, поведение не определено, если такое выражение неупорядоченный побочный эффект возникает в любом из заказов.84)

6.5/3

группировка операторов и операндов указывается синтаксис.Восемьдесят пять) За исключением как определено позже, побочные эффекты и вычисления значения подвыражения не имеют последовательности.86)

вы не должны полагаться на неопределенное поведение.

некоторые альтернативы: в C++ вы можете использовать

  std::swap(a, b);

XOR-swap:

  a = a^b;
  b = a^b;
  a = a^b;

проблема в том, что согласно стандарту C++

за исключением отмеченных случаев, оценки операндов отдельных операторов и подвыражения отдельных выражений не имеют последовательности.

Так это выражение

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

имеет неопределенное поведение.

можно использовать алгоритм замены XOR предотвратить все вопросы переполнения и все еще иметь ОДН-вкладыш.

но, так как у вас есть c++ тег, я бы предпочел просто std::swap(a, b) чтобы сделать его более легко читаемым.

помимо проблем, вызванных другими пользователями, этот тип подкачки имеет еще одну проблему:домен.

, что если a+b выходит за его пределы? Допустим, мы работаем с номером от 0 to 100. 80+ 60 выйдет за пределы досягаемости.