Он не определен, потому что он изменяется x
дважды между точками последовательности. Стандарт говорит, что он не определен, поэтому он не определен.
Это много я знаю.
Но почему?
Я понимаю, что запрет этого позволяет компиляторам оптимизировать лучше. Это могло иметь смысл, когда C был изобретен, но теперь кажется слабым аргументом.
Если бы мы сегодня заново изобрели C, сделаем ли мы это так или лучше?
Или, может быть, есть более глубокая проблема, которая затрудняет определение согласованных правил для таких выражений, поэтому лучше их запретить?
Итак, предположим, что мы должны были изобрести C сегодня. Я хотел бы предложить простые правила для выражений, таких как x=x++
, которые, как мне кажется, работают лучше, чем существующие правила.
Я хотел бы узнать ваше мнение о предлагаемых правилах по сравнению с существующими или другими предложениями.
Предлагаемые правила:
- Между точками последовательности порядок оценки не указан.
- Побочные эффекты имеют место немедленно.
Там нет неопределенного поведения. Выражения оцениваются в ту или иную величину, но, конечно, не отформатируют ваш жесткий диск (странно, я никогда не видел реализацию, где x=x++
форматирует жесткий диск).
Примеры выражений
x=x++
- Четко определено, не меняетсяx
.
Сначалаx
увеличивается (сразу послеx++
оценки), затем сохраняется его старое значениеx
.x++ + ++x
- Увеличивает вx
два раза, оценивает до2*x+2
.
Хотя любая из сторон может быть оценена первой, результатом будет либоx + (x+2)
(левая сторона первой), либо(x+1) + (x+1)
(правая сторона первой).x = x + (x=3)
- Не указано,x
установлено либо либо,x+3
либо6
.
Если правая сторона оценивается первой, этоx+3
. Также возможно, чтоx=3
оценивается первым, так и есть3+3
. В любом случаеx=3
присвоение происходит сразу послеx=3
оценки, поэтому сохраненное значение перезаписывается другим назначением.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 и посмотреть, насколько это исключение смущает людей.
Вот я и говорю - избавьтесь от этой путаницы.
=== Сводка ответов ===
Зачем это делать?
Я попытался объяснить в разделе выше - я хочу, чтобы правила C были простыми.Потенциал для оптимизации:
это отнимает некоторую свободу у компилятора, но я не увидел ничего, что убедило бы меня в том, что это может быть важно.
Большинство оптимизаций еще можно сделать. Например,a=3;b=5;
могут быть переупорядочены, даже если стандарт определяет порядок. Выражения, такие как,a=b[i++]
все еще могут быть оптимизированы аналогичным образом.Вы не можете изменить существующий стандарт.
Я признаю, я не могу. Я никогда не думал, что смогу пойти дальше и изменить стандарты и компиляторы. Я только хотел подумать, если бы все могло быть сделано иначе.
источник
x
себе, и если вы хотите увеличить,x
вы можете просто сказатьx++;
- нет необходимости в присваивании. Я бы сказал, что это не следует определять только потому, что трудно вспомнить, что должно было случиться.Ответы:
Может быть, вы должны сначала ответить на вопрос, почему это должно быть определено? Есть ли какое-либо преимущество в стиле программирования, удобочитаемости, удобстве сопровождения или производительности, позволяя таким выражениям создавать дополнительные побочные эффекты? Является
более читабельным, чем
Учитывая, что такое изменение является чрезвычайно фундаментальным и нарушает существующую кодовую базу.
источник
Аргумент, что создание такого неопределенного поведения позволяет лучше оптимизировать, не является слабым сегодня. На самом деле, сегодня он намного сильнее, чем когда С был новым.
Когда C был новым, машины, которые могли воспользоваться этим для лучшей оптимизации, были в основном теоретическими моделями. Люди говорили о возможности создания процессоров, где компилятор будет инструктировать процессор о том, какие инструкции можно / нужно выполнять параллельно с другими инструкциями. Они указали на тот факт, что разрешение этого поведения иметь неопределенное поведение означало, что на таком ЦП, если он вообще когда-либо существовал, вы могли запланировать выполнение части «приращения» команды параллельно с остальной частью потока команд. Хотя они были правы в теории, в то время было мало аппаратных средств, которые могли бы реально воспользоваться этой возможностью.
Это больше не просто теоретическое. Теперь есть аппаратное обеспечение в производстве и широком использовании (например, Itanium, VLIW DSP), которое действительно может воспользоваться этим. Они действительно делают позволяют компилятору генерировать поток команд , который указывает , что инструкции X, Y и Z могут быть выполнены параллельно. Это уже не теоретическая модель - это реальное аппаратное обеспечение в реальном использовании, выполняющее реальную работу.
ИМО, создание этого определенного поведения близко к худшему из возможных «решений» проблемы. Вы явно не должны использовать такие выражения. Для подавляющего большинства кода идеальным поведением было бы то, что компилятор просто полностью отклонял бы такие выражения. В то время компиляторы C не делали анализ потока, необходимый для надежного обнаружения этого. Даже во времена оригинального стандарта C это все еще не было распространено.
Я не уверен, что сегодня это будет приемлемо и для сообщества - хотя многие компиляторы могут выполнять такой анализ потоков, они обычно делают это только тогда, когда вы запрашиваете оптимизацию. Я сомневаюсь, что большинству программистов понравилась бы идея замедления «отладочных» сборок только ради возможности отклонить код, который они (будучи в здравом уме), никогда не писали бы в первую очередь.
То, что сделал C, - это полу-разумный второй лучший выбор: скажите людям не делать этого, позволяя (но не требуя) компилятору отклонять код. Это позволяет избежать (еще более) замедления компиляции для людей, которые никогда не будут его использовать, но все же позволяет кому-то написать компилятор, который будет отклонять такой код, если он этого захочет (и / или иметь флаги, которые будут отклонять его, что люди могут выбрать для использования). или нет, как они считают нужным).
По крайней мере, IMO, создание этого определенного поведения было бы (по крайней мере, близко) к худшему из возможных решений. На оборудовании в стиле VLIW вы могли бы генерировать более медленный код для разумного использования операторов приращения, просто ради дрянного кода, который злоупотребляет ими, или же всегда требуется подробный анализ потока, чтобы доказать, что вы не имеете дело с дрянной код, поэтому вы можете создавать медленный (сериализованный) код только тогда, когда это действительно необходимо.
Итог: если вы хотите решить эту проблему, вы должны думать в противоположном направлении. Вместо того, чтобы определять, что делает такой код, вы должны определить язык, чтобы такие выражения просто не допускались вообще (и согласились с тем фактом, что большинство программистов, вероятно, выберут более быструю компиляцию, чем выполнение этого требования).
источник
a=b[i++];
(для одного примера) - это хорошо, а оптимизация - это хорошо. Я, однако, не вижу смысла вредить разумному коду как таковому, чтобы что-то подобное++i++
имело определенный смысл.++i++
состоит в том, что вообще трудно отличить их от допустимых выражений с побочными эффектами (такими какa=b[i++]
). Для нас это может показаться достаточно простым, но если я правильно помню « Книгу Дракона», то это на самом деле проблема NP-сложности. Вот почему это поведение UB, а не запрещено.Эрик Липперт, главный дизайнер команды компиляторов C #, опубликовал в своем блоге статью о ряде соображений, касающихся выбора, чтобы сделать функцию неопределенной на уровне спецификации языка. Очевидно, что C # - это другой язык, с различными факторами, влияющими на его языковой дизайн, но, тем не менее, его замечания актуальны.
В частности, он указывает на проблему наличия существующих компиляторов для языка, которые имеют существующие реализации, а также имеют представителей в комитете. Я не уверен, что это так, но имеет отношение к большинству спецификаций, связанных с C и C ++.
Также стоит отметить, как вы сказали, потенциал производительности для оптимизации компилятора. Несмотря на то, что в наши дни производительность процессоров на много порядков выше, чем в те времена, когда C был молодым, в наши дни большое количество программ на C выполняется именно из-за потенциального прироста производительности и потенциального (гипотетического будущего). ) Было бы глупо исключать оптимизацию команд ЦП и многоядерную обработку из-за чрезмерно ограничительного набора правил для обработки побочных эффектов и точек последовательности.
источник
Во-первых, давайте посмотрим на определение неопределенного поведения:
Другими словами, «неопределенное поведение» просто означает, что компилятор может свободно обрабатывать ситуацию любым удобным для него способом, и любое такое действие считается «правильным».
Корень обсуждаемой проблемы заключается в следующем пункте:
Акцент добавлен.
Учитывая выражение как
подвыражения
a++
,--b
,c
и++d
могут быть оценены в любом порядке . Кроме того, побочные эффектыa++
,--b
и++d
могут быть применены в любой момент до следующей точки последовательности (IOW, даже еслиa++
оцениваются , прежде чем--b
, это не гарантирует , чтоa
будет обновлена , прежде чем--b
оцениваются). Как уже говорили другие, обоснование такого поведения состоит в том, чтобы дать реализации свободу переупорядочивать операции оптимальным образом.Из-за этого, однако, такие выражения, как
и т. д., даст разные результаты для разных реализаций (или для одной и той же реализации с разными настройками оптимизации или на основе окружающего кода и т. д.).
Поведение остается неопределенным, так что компилятор не обязан «делать правильные вещи», что бы это ни было. Вышеперечисленные случаи достаточно легко выявляются, но есть нетривиальное количество случаев, которые было бы трудно или невозможно отловить во время компиляции.
Очевидно, вы можете спроектировать язык так, чтобы порядок оценки и порядок применения побочных эффектов были строго определены, а Java и C # делают это, в основном, чтобы избежать проблем, к которым приводят определения C и C ++.
Итак, почему это изменение не было внесено в C после 3 стандартных ревизий? Прежде всего, существует унаследованный 40-летний код C, и не гарантируется, что такое изменение не нарушит этот код. Это накладывает некоторую нагрузку на разработчиков компиляторов, поскольку такое изменение немедленно сделает все существующие компиляторы несоответствующими; каждый должен будет сделать значительную переписку. И даже на быстрых современных процессорах все еще можно добиться реального прироста производительности, изменив порядок оценки.
источник
Во-первых, вы должны понять, что это не просто x = x ++, который не определен. Никто не заботится о x = x ++, так как не имеет значения, к чему бы вы его ни определили. То, что не определено, больше похоже на «a = b ++, где a и b совпадают» - т.е.
Существует несколько различных способов реализации этой функции в зависимости от того, что наиболее эффективно для архитектуры процессора (и для окружающих операторов, в случае, если это более сложная функция, чем в примере). Например, два очевидных:
или
Обратите внимание, что первый из перечисленных выше, тот, который использует больше инструкций и больше регистров, - это тот, который вам потребуется использовать во всех случаях, когда невозможно доказать, что a и b различны.
источник
b
раньшеa
.наследие
Предположение, что C может быть изобретен сегодня, не может быть верным. Существует так много строк C-кодов, которые были созданы и используются ежедневно, что изменение правил игры в середине игры просто неправильно.
Конечно, вы можете изобрести новый язык, скажем C + = , со своими правилами. Но это не будет C.
источник
Объявление о том, что что-то определено, не изменит существующие компиляторы для соответствия вашему определению. Это особенно верно в случае предположения, на которое можно было положиться явно или неявно во многих местах.
Основная проблема для этого предположения не в том
x = x++;
(компиляторы могут легко проверить это и должны предупреждать),*p1 = (*p2)++
а вp1[i] = p2[j]++;
том, что компилятор не может легко узнать, еслиp1 == p2
(в C99, p1 и p2 являются параметрами функции)restrict
была добавлена, чтобы распространить возможность предположить, что p1! = p2 между точками последовательности, поэтому считалось, что возможности оптимизации были важны).источник
p1[i]=p2[j]++
. Если компилятор может предполагать отсутствие псевдонимов, это не проблема. Если он не может, он должен идти по книге -p2[j]
сначала увеличивай ,p1[i]
потом сохраняй . За исключением потерянных возможностей оптимизации, которые не кажутся существенными, я не вижу проблем.x = x++;
которое не было написано, ноt = x; x++; x = t;
илиx=x; x++;
или что вы хотите в качестве семантики (но как насчет диагностики?). Для нового языка просто пропустите побочные эффекты.x++
как точку последовательности, как если бы это был вызов функцииinc_and_return_old(&x)
.В некоторых случаях этот вид кода был определен в новом стандарте C ++ 11.
источник
x = ++x
теперь четко определены (но неx = x++
)