Почему 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;
}

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

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

Ответ 1

Нет. Это неприемлемо. Этот код вызывает Undefined поведение. Это связано с тем, что операция на b не определена. В выражении

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

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

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

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

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


<суб > 1. Акцент мой.

Ответ 2

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

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

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

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

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

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

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

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

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

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

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

Ответ 3

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

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

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

Ответ 4

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

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

Ответ 5

Если вы используете gcc и -Wall, компилятор уже предупреждает вас

a.c: 3: 26: предупреждение: операция с 'b может быть undefined [-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

вы не видите никакой пользы для обфускации кода.


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

Ответ 6

Заявление:

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

вызывает поведение undefined. Второй нарушается в цитируемом параграфе:

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

Ответ 7

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

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

Стив Джессоп предупреждает об этом:

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

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

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

a = x + y;

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

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

Теперь для стандартных котировок C11 от N1570:

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

Это неуказанное поведение, когда:

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

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

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

Это поведение undefined, когда:

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

6.5/1

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

6.5/2

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

6.5/3

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

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

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

  std::swap(a, b);

XOR замены:

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

Ответ 8

Вы можете использовать алгоритм обмена XOR, чтобы предотвратить любые проблемы с переполнением и все еще иметь однострочный файл.

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

Ответ 9

Проблема заключается в том, что согласно стандарту С++

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

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

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

имеет undefined behavioir.

Ответ 10

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

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