Почему x = x ++ не определено?

19

Он не определен, потому что он изменяется xдважды между точками последовательности. Стандарт говорит, что он не определен, поэтому он не определен.
Это много я знаю.

Но почему?

Я понимаю, что запрет этого позволяет компиляторам оптимизировать лучше. Это могло иметь смысл, когда C был изобретен, но теперь кажется слабым аргументом.
Если бы мы сегодня заново изобрели C, сделаем ли мы это так или лучше?
Или, может быть, есть более глубокая проблема, которая затрудняет определение согласованных правил для таких выражений, поэтому лучше их запретить?

Итак, предположим, что мы должны были изобрести C сегодня. Я хотел бы предложить простые правила для выражений, таких как x=x++, которые, как мне кажется, работают лучше, чем существующие правила.
Я хотел бы узнать ваше мнение о предлагаемых правилах по сравнению с существующими или другими предложениями.

Предлагаемые правила:

  1. Между точками последовательности порядок оценки не указан.
  2. Побочные эффекты имеют место немедленно.

Там нет неопределенного поведения. Выражения оцениваются в ту или иную величину, но, конечно, не отформатируют ваш жесткий диск (странно, я никогда не видел реализацию, где x=x++форматирует жесткий диск).

Примеры выражений

  1. x=x++- Четко определено, не меняется x.
    Сначала xувеличивается (сразу после x++оценки), затем сохраняется его старое значение x.

  2. x++ + ++x- Увеличивает в xдва раза, оценивает до 2*x+2.
    Хотя любая из сторон может быть оценена первой, результатом будет либо x + (x+2)(левая сторона первой), либо (x+1) + (x+1)(правая сторона первой).

  3. x = x + (x=3)- Не указано, xустановлено либо либо, x+3либо 6.
    Если правая сторона оценивается первой, это x+3. Также возможно, что x=3оценивается первым, так и есть 3+3. В любом случае x=3присвоение происходит сразу после x=3оценки, поэтому сохраненное значение перезаписывается другим назначением.

  4. x+=(x=3)- Хорошо определено, устанавливается xв 6.
    Вы можете утверждать, что это просто сокращение для выражения выше.
    Но я бы сказал, что это +=должно быть выполнено после x=3, а не в двух частях (чтение x, оценка x=3, добавление и сохранение нового значения).

В чем преимущество?

Некоторые комментарии подняли этот хороший момент.
Я, конечно, не думаю, что такие выражения, как x=x++следует использовать в любом нормальном коде.
На самом деле, я гораздо более строг, чем это - я думаю, что единственное хорошее использование для x++в x++;одиночку.

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

Основное правило таково:
если A допустимо, а B допустимо и они правильно объединены, результат действителен.
xявляется допустимым L-значением, x++является допустимым выражением и =является допустимым способом объединения L-значения и выражения, так почему же x=x++это не законно?
Стандарт C делает здесь исключение, и это исключение усложняет правила. Вы можете поискать на stackoverflow.com и посмотреть, насколько это исключение смущает людей.
Вот я и говорю - избавьтесь от этой путаницы.

=== Сводка ответов ===

  1. Зачем это делать?
    Я попытался объяснить в разделе выше - я хочу, чтобы правила C были простыми.

  2. Потенциал для оптимизации:
    это отнимает некоторую свободу у компилятора, но я не увидел ничего, что убедило бы меня в том, что это может быть важно.
    Большинство оптимизаций еще можно сделать. Например, a=3;b=5;могут быть переупорядочены, даже если стандарт определяет порядок. Выражения, такие как, a=b[i++]все еще могут быть оптимизированы аналогичным образом.

  3. Вы не можете изменить существующий стандарт.
    Я признаю, я не могу. Я никогда не думал, что смогу пойти дальше и изменить стандарты и компиляторы. Я только хотел подумать, если бы все могло быть сделано иначе.

ugoren
источник
10
Почему это так важно для тебя? Должно ли оно быть определено, и если да, то почему? Нет особого смысла присваивать xсебе, и если вы хотите увеличить, xвы можете просто сказать x++;- нет необходимости в присваивании. Я бы сказал, что это не следует определять только потому, что трудно вспомнить, что должно было случиться.
Калеб
4
На мой взгляд, это хороший вопрос («Некоторые люди видят вещи такими, какие они есть, и спрашивают, почему, я мечтаю о вещах, которых никогда не было, и спрашиваю, почему нет»). Это (на мой взгляд) вопрос исключительно о дизайне языка, использующий синтаксис C в качестве примера, а не вопрос о синтаксисе C. Лично я думаю, что причина, по которой мы не определили поведение для таких выражений, как x ++ + ++ x или x = x ++, заключается просто в том, что существует вероятность того, что они будут неправильно прочитаны.
Джейми Тейлор
5
@ Югорен: Зачем вам нужно прогнозировать результат. Никто в здравом уме не написал бы такой код (как уже упоминалось несколько раз), даже если бы вы писали такой код, он будет отклонен при первом рассмотрении кода. Поэтому нет необходимости определять поведение и давать оптимизатору наилучшие шансы на его оптимизацию. В каждом предложенном вами примере я бы застрелил кого-нибудь, если бы он добавил это в базу кода.
Мартин Йорк
3
Я нашел бы более интересный вопрос, почему бы не написать это? Конечно, компилятор может обнаружить его неопределенное поведение и, следовательно, не может быть тем, что на самом деле хотел пользователь, так почему же нет ошибки? Я понимаю, что некоторые случаи неопределенного бахавора трудно обнаружить, но это не так; t
JohnB
3
« правило, запрещающее изменение переменной дважды между точками последовательности, безусловно, является правилом, которого большинство программистов не понимают». - У вас есть доказательства этому? Большинство вопросов, которые я видел, были заданы, потому что программисты не знали о правиле. Есть ли доказательства того, что большинство из них до сих пор не поняли это после того, как это объяснили?
Безопасное

Ответы:

24

Может быть, вы должны сначала ответить на вопрос, почему это должно быть определено? Есть ли какое-либо преимущество в стиле программирования, удобочитаемости, удобстве сопровождения или производительности, позволяя таким выражениям создавать дополнительные побочные эффекты? Является

y = x++ + ++x;

более читабельным, чем

y = 2*x + 2;
x += 2;

Учитывая, что такое изменение является чрезвычайно фундаментальным и нарушает существующую кодовую базу.

Безопасный
источник
1
Я добавил раздел «почему» в свой вопрос. Я, конечно, не предлагаю использовать эти выражения, но мне интересно иметь простые правила, чтобы определить значение выражения.
Угорен
Кроме того, это изменение не нарушает существующий код, если только оно не вызывает неопределенное поведение. Поправьте меня если я ошибаюсь.
Угорен
3
Ну, более философский ответ: в настоящее время он не определен. Если ни один программист не использует его, вам не нужно понимать такие выражения, потому что не должно быть никакого кода. Если вам необходимо понять их, то, очевидно, должно быть много кода, основанного на неопределенном поведении. ;)
Безопасное
1
По определению, он не нарушает существующую кодовую базу для определения поведения. Если они содержали UB, они по определению были уже сломаны.
DeadMG
1
@ugoren: Ваш раздел «почему» все еще не отвечает на практический вопрос: зачем вам это странное выражение в вашем коде? Если вы не можете найти убедительного ответа на этот вопрос, тогда вся дискуссия спорна.
Майк Баранчак
20

Аргумент, что создание такого неопределенного поведения позволяет лучше оптимизировать, не является слабым сегодня. На самом деле, сегодня он намного сильнее, чем когда С был новым.

Когда C был новым, машины, которые могли воспользоваться этим для лучшей оптимизации, были в основном теоретическими моделями. Люди говорили о возможности создания процессоров, где компилятор будет инструктировать процессор о том, какие инструкции можно / нужно выполнять параллельно с другими инструкциями. Они указали на тот факт, что разрешение этого поведения иметь неопределенное поведение означало, что на таком ЦП, если он вообще когда-либо существовал, вы могли запланировать выполнение части «приращения» команды параллельно с остальной частью потока команд. Хотя они были правы в теории, в то время было мало аппаратных средств, которые могли бы реально воспользоваться этой возможностью.

Это больше не просто теоретическое. Теперь есть аппаратное обеспечение в производстве и широком использовании (например, Itanium, VLIW DSP), которое действительно может воспользоваться этим. Они действительно делают позволяют компилятору генерировать поток команд , который указывает , что инструкции X, Y и Z могут быть выполнены параллельно. Это уже не теоретическая модель - это реальное аппаратное обеспечение в реальном использовании, выполняющее реальную работу.

ИМО, создание этого определенного поведения близко к худшему из возможных «решений» проблемы. Вы явно не должны использовать такие выражения. Для подавляющего большинства кода идеальным поведением было бы то, что компилятор просто полностью отклонял бы такие выражения. В то время компиляторы C не делали анализ потока, необходимый для надежного обнаружения этого. Даже во времена оригинального стандарта C это все еще не было распространено.

Я не уверен, что сегодня это будет приемлемо и для сообщества - хотя многие компиляторы могут выполнять такой анализ потоков, они обычно делают это только тогда, когда вы запрашиваете оптимизацию. Я сомневаюсь, что большинству программистов понравилась бы идея замедления «отладочных» сборок только ради возможности отклонить код, который они (будучи в здравом уме), никогда не писали бы в первую очередь.

То, что сделал C, - это полу-разумный второй лучший выбор: скажите людям не делать этого, позволяя (но не требуя) компилятору отклонять код. Это позволяет избежать (еще более) замедления компиляции для людей, которые никогда не будут его использовать, но все же позволяет кому-то написать компилятор, который будет отклонять такой код, если он этого захочет (и / или иметь флаги, которые будут отклонять его, что люди могут выбрать для использования). или нет, как они считают нужным).

По крайней мере, IMO, создание этого определенного поведения было бы (по крайней мере, близко) к худшему из возможных решений. На оборудовании в стиле VLIW вы могли бы генерировать более медленный код для разумного использования операторов приращения, просто ради дрянного кода, который злоупотребляет ими, или же всегда требуется подробный анализ потока, чтобы доказать, что вы не имеете дело с дрянной код, поэтому вы можете создавать медленный (сериализованный) код только тогда, когда это действительно необходимо.

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

Джерри Гроб
источник
ИМО, нет оснований полагать, что в большинстве случаев более медленные инструкции действительно намного медленнее быстрых инструкций и что они всегда будут влиять на производительность программы. Я бы отнес этот класс к преждевременной оптимизации.
DeadMG
Может быть, я что-то упускаю - если никто никогда не должен был писать такой код, зачем заботиться об его оптимизации?
Угорен
1
@ugoren: написание кода вроде a=b[i++];(для одного примера) - это хорошо, а оптимизация - это хорошо. Я, однако, не вижу смысла вредить разумному коду как таковому, чтобы что-то подобное ++i++имело определенный смысл.
Джерри Гроб
2
@ugoren Проблема заключается в диагностике. Единственная цель не запрещать выражения, такие как, в частности, ++i++состоит в том, что вообще трудно отличить их от допустимых выражений с побочными эффектами (такими как a=b[i++]). Для нас это может показаться достаточно простым, но если я правильно помню « Книгу Дракона», то это на самом деле проблема NP-сложности. Вот почему это поведение UB, а не запрещено.
Конрад Рудольф
1
Я не верю, что выступление является веским аргументом. Я изо всех сил стараюсь поверить, что дело достаточно распространенное, учитывая очень небольшую разницу и очень быстрое выполнение в обоих случаях, чтобы заметить небольшое падение производительности, не говоря уже о том, что на многих процессорах и архитектурах его определение эффективно бесплатно.
DeadMG
9

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

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

Также стоит отметить, как вы сказали, потенциал производительности для оптимизации компилятора. Несмотря на то, что в наши дни производительность процессоров на много порядков выше, чем в те времена, когда C был молодым, в наши дни большое количество программ на C выполняется именно из-за потенциального прироста производительности и потенциального (гипотетического будущего). ) Было бы глупо исключать оптимизацию команд ЦП и многоядерную обработку из-за чрезмерно ограничительного набора правил для обработки побочных эффектов и точек последовательности.

Tanzelax
источник
Из статьи, на которую вы ссылаетесь, кажется, что C # недалеко от того, что я предлагаю. Порядок побочных эффектов определяется «когда наблюдается из потока, который вызывает побочные эффекты». Я не упомянул многопоточность, но в целом C не гарантирует многого для наблюдателя в другом потоке.
Угорен
5

Во-первых, давайте посмотрим на определение неопределенного поведения:

3.4.3

1 неопределенное поведенческое
поведение при использовании непереносимой или ошибочной программной конструкции или ошибочных данных, для которых настоящий международный стандарт не предъявляет никаких требований

2 ПРИМЕЧАНИЕ Возможное неопределенное поведение варьируется от полного игнорирования ситуации с непредсказуемыми результатами до поведения во время перевода или выполнения программы в документированная характеристика, характерная для среды (с выдачей диагностического сообщения или без нее), для прекращения перевода или выполнения (с выдачей диагностического сообщения).

3 Пример. Примером неопределенного поведения является поведение на целочисленном потоке.

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

Корень обсуждаемой проблемы заключается в следующем пункте:

6.5 Выражения

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

Акцент добавлен.

Учитывая выражение как

x = a++ * --b / (c + ++d);

подвыражения a++, --b, cи ++dмогут быть оценены в любом порядке . Кроме того, побочные эффекты a++, --bи ++dмогут быть применены в любой момент до следующей точки последовательности (IOW, даже если a++оцениваются , прежде чем --b, это не гарантирует , что aбудет обновлена , прежде чем --bоцениваются). Как уже говорили другие, обоснование такого поведения состоит в том, чтобы дать реализации свободу переупорядочивать операции оптимальным образом.

Из-за этого, однако, такие выражения, как

x = x++
y = i++ * i++
a[i] = i++
*p++ = -*p    // this one bit me just yesterday

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

Поведение остается неопределенным, так что компилятор не обязан «делать правильные вещи», что бы это ни было. Вышеперечисленные случаи достаточно легко выявляются, но есть нетривиальное количество случаев, которые было бы трудно или невозможно отловить во время компиляции.

Очевидно, вы можете спроектировать язык так, чтобы порядок оценки и порядок применения побочных эффектов были строго определены, а Java и C # делают это, в основном, чтобы избежать проблем, к которым приводят определения C и C ++.

Итак, почему это изменение не было внесено в C после 3 стандартных ревизий? Прежде всего, существует унаследованный 40-летний код C, и не гарантируется, что такое изменение не нарушит этот код. Это накладывает некоторую нагрузку на разработчиков компиляторов, поскольку такое изменение немедленно сделает все существующие компиляторы несоответствующими; каждый должен будет сделать значительную переписку. И даже на быстрых современных процессорах все еще можно добиться реального прироста производительности, изменив порядок оценки.

Джон Боде
источник
1
Очень хорошее объяснение проблемы. Я не согласен с тем, чтобы ломать устаревшие приложения - способ реализации неопределенного / неопределенного поведения иногда меняется в зависимости от версии компилятора, без каких-либо изменений в стандарте. Я не предлагаю изменить какое-либо определенное поведение.
Угорен
4

Во-первых, вы должны понять, что это не просто x = x ++, который не определен. Никто не заботится о x = x ++, так как не имеет значения, к чему бы вы его ни определили. То, что не определено, больше похоже на «a = b ++, где a и b совпадают» - т.е.

void f(int *a, int *b) {
    *a = (*b)++;
}
int i;
f(&i, &i);

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

load r1 = *b
copy r2 = r1
increment r1
store *b = r1
store *a = r2

или

load r1 = *b
store *a = r1
increment r1
store *b = r1

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

Random832
источник
Вы действительно показываете случай, когда мое предложение приводит к большему количеству машинных операций, но для меня оно выглядит незначительным. И у компилятора все еще есть некоторая свобода - единственное реальное требование, которое я добавляю, - хранить его bраньше a.
Угорен
3

наследие

Предположение, что C может быть изобретен сегодня, не может быть верным. Существует так много строк C-кодов, которые были созданы и используются ежедневно, что изменение правил игры в середине игры просто неправильно.

Конечно, вы можете изобрести новый язык, скажем C + = , со своими правилами. Но это не будет C.

mouviciel
источник
2
Я не думаю, что мы можем изобрести C сегодня. Это не значит, что мы не можем обсуждать эти вопросы. Однако то, что я предлагаю, на самом деле не изобретает заново. Преобразование неопределенного поведения в определенное или неопределенное может быть выполнено при обновлении стандарта, и язык по-прежнему будет C.
ugoren
2

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

Основная проблема для этого предположения не в том x = x++;(компиляторы могут легко проверить это и должны предупреждать), *p1 = (*p2)++а в p1[i] = p2[j]++;том, что компилятор не может легко узнать, если p1 == p2(в C99, p1 и p2 являются параметрами функции) restrictбыла добавлена, чтобы распространить возможность предположить, что p1! = p2 между точками последовательности, поэтому считалось, что возможности оптимизации были важны).

AProgrammer
источник
Я не понимаю, как мое предложение что-то меняет в отношении p1[i]=p2[j]++. Если компилятор может предполагать отсутствие псевдонимов, это не проблема. Если он не может, он должен идти по книге - p2[j]сначала увеличивай , p1[i]потом сохраняй . За исключением потерянных возможностей оптимизации, которые не кажутся существенными, я не вижу проблем.
Угорен
Второй абзац не был независим от первого, но был примером того, в каких местах допущение может проникнуть и его будет сложно отследить.
AProgrammer
В первом абзаце говорится о чем-то совершенно очевидном - компиляторы должны быть изменены, чтобы соответствовать новому стандарту. Я не думаю, что у меня есть шанс стандартизировать это и заставить авторов компиляторов следовать. Я просто думаю, что это стоит обсудить.
Угорен
Проблема не в том, что нужно менять компиляторы при любом изменении языка, а в том, что эти изменения распространены и их трудно найти. Наиболее практичным подходом, вероятно, было бы изменение промежуточного формата, в котором работает оптимизатор, то есть притворство, x = x++;которое не было написано, но t = x; x++; x = t;или x=x; x++;или что вы хотите в качестве семантики (но как насчет диагностики?). Для нового языка просто пропустите побочные эффекты.
AProgrammer
Я не слишком много знаю о структуре компилятора. Если бы я действительно хотел изменить все компиляторы, мне было бы все равно. Но, может быть, рассматривать x++как точку последовательности, как если бы это был вызов функции inc_and_return_old(&x).
Угорен
-1

В некоторых случаях этот вид кода был определен в новом стандарте C ++ 11.

DeadMG
источник
5
Хотите разработать?
Угорен
Я думаю, что x = ++xтеперь четко определены (но не x = x++)
ММ