Я читал о порядке нарушений оценки , и они приводят пример, который озадачивает меня.
1) Если побочный эффект на скалярный объект не секвенирован относительно другого побочного эффекта на тот же скалярный объект, поведение не определено.
// snip f(i = -1, i = -1); // undefined behavior
В этом контексте i
это скалярный объект , который, по-видимому, означает
Арифметические типы (3.9.1), типы перечисления, типы указателей, указатели на типы элементов (3.9.2), std :: nullptr_t и cv-квалифицированные версии этих типов (3.9.3) вместе называются скалярными типами.
Я не вижу, как это утверждение неоднозначно в этом случае. Мне кажется, что независимо от того, оценивается ли первый или второй аргумент первым, он i
заканчивается как -1
, и оба аргумента также -1
.
Может кто-нибудь уточнить, пожалуйста?
ОБНОВИТЬ
Я действительно ценю все обсуждения. До сих пор мне очень нравится ответ @ damaic, так как он раскрывает подводные камни и тонкости определения этого утверждения, несмотря на то, насколько прямолинейно оно выглядит на первый взгляд. @ acheong87 указывает на некоторые проблемы, которые возникают при использовании ссылок, но я думаю, что это ортогонально аспекту побочных эффектов этого вопроса.
РЕЗЮМЕ
Поскольку этот вопрос получил массу внимания, я суммирую основные моменты / ответы. Во-первых, позвольте мне сделать небольшое отступление, чтобы указать, что «почему» может иметь тесно связанные, но слегка различающиеся значения, а именно «по какой причине », «по какой причине » и «с какой целью ». Я сгруппирую ответы, по которым из этих значений «почему» они обращались.
по какой причине
Основной ответ здесь приходит от Пола Дрейпера , с Мартином Дж, который внес аналогичный, но не столь обширный ответ. Ответ Пола Дрейпера сводится к
Это неопределенное поведение, потому что не определено, что это за поведение.
Ответ в целом очень хороший с точки зрения объяснения того, что говорит стандарт C ++. Также рассматриваются некоторые связанные случаи UB, такие как f(++i, ++i);
и f(i=1, i=-1);
. В первом из связанных случаев не ясно, должен ли первый аргумент быть, i+1
а второй i+2
или наоборот; во-вторых, не ясно, i
должно ли быть 1 или -1 после вызова функции. Оба эти случая являются UB, потому что они подпадают под следующее правило:
Если побочный эффект на скалярный объект не секвенирован относительно другого побочного эффекта на тот же скалярный объект, поведение не определено.
Следовательно, f(i=-1, i=-1)
это также UB, поскольку он подпадает под то же правило, несмотря на то, что намерение программиста является (IMHO) очевидным и однозначным.
Пол Дрэйпер также ясно указывает в своем заключении, что
Могло ли это быть определено поведение? Да. Это было определено? Нет.
что приводит нас к вопросу «по какой причине / цели было f(i=-1, i=-1)
оставлено неопределенное поведение?»
по какой причине / цели
Хотя в стандарте C ++ есть некоторые упущения (возможно, небрежные), многие упущения хорошо обоснованы и служат определенной цели. Хотя я знаю, что цель часто заключается в том, чтобы «облегчить работу автора-компилятора» или «ускорить код», мне было интересно узнать, есть ли веская причина оставить f(i=-1, i=-1)
UB.
вредный и суперкат дают основные ответы, которые дают повод для UB. Harmic отмечает, что оптимизирующий компилятор, который может разбить якобы атомарные операции присваивания на несколько машинных инструкций, и что он может дополнительно чередовать эти инструкции для оптимальной скорости. Это может привести к некоторым очень удивительным результатам: i
в его сценарии он заканчивается как -2! Таким образом, вредный код демонстрирует, как назначение одного и того же значения переменной более одного раза может иметь негативные последствия, если операции не выполняются.
Суперкат представляет собой связное изложение подводных камней, пытающихся добиться того, f(i=-1, i=-1)
на что он похож. Он указывает, что на некоторых архитектурах существуют жесткие ограничения на одновременную запись в один и тот же адрес памяти. Компилятору может быть трудно поймать это, если мы имеем дело с чем-то менее тривиальным, чем f(i=-1, i=-1)
.
Дэвидф также приводит пример инструкций чередования, очень похожих на вредные.
Хотя каждый из примеров вредителей, суперкатов и дэвидфов несколько надуман, вместе взятые они все же служат обоснованной причиной, почему f(i=-1, i=-1)
должно быть неопределенное поведение.
Я принял ответ вредителя, потому что он наилучшим образом справился со всеми смыслами того, почему, хотя в ответе Пола Дрейпера был лучше проработан раздел «по какой причине».
другие ответы
ДжонБ указывает, что если мы рассмотрим перегруженные операторы присваивания (а не просто простые скаляры), то у нас тоже могут возникнуть проблемы.
источник
std::nullptr_t
и cv-квалифицированные версии этих типов (3.9.3) вместе называются скалярными типами . "f(i-1, i = -1)
или что-то подобное.Ответы:
Поскольку операции не упорядочены, нечего сказать, что инструкции, выполняющие назначение, не могут чередоваться. Это может быть оптимально, в зависимости от архитектуры процессора. Ссылочная страница утверждает это:
Само по себе это не похоже на проблему - предполагается, что выполняемая операция сохраняет значение -1 в ячейке памяти. Но также нечего сказать, что компилятор не может оптимизировать это в отдельный набор инструкций, который имеет тот же эффект, но который может потерпеть неудачу, если операция была перемежена с другой операцией в той же области памяти.
Например, представьте, что было бы более эффективно обнулить память, затем уменьшить ее по сравнению с загрузкой значения -1 в. Тогда это:
может стать:
Теперь мне -2.
Это, вероятно, фиктивный пример, но это возможно.
источник
load 8bit immediate and shift
до 4 раз. Обычно компилятор выполняет косвенную адресацию для извлечения числа из таблицы, чтобы избежать этого. (-1 можно сделать в 1 инструкции, но можно выбрать другой пример).Во-первых, «скалярный объект» означает тип, такой как
int
,float
или указатель (см. Что такое скалярный объект в C ++? ).Во-вторых, может показаться более очевидным, что
будет иметь неопределенное поведение. Но
менее очевидно.
Немного другой пример:
Какое назначение произошло «последним»
i = 1
, илиi = -1
? Это не определено в стандарте. В самом деле, этоi
может быть5
(см. Ответ вредника для вполне правдоподобного объяснения того, как это может иметь место). Или ваша программа может segfault. Или переформатируйте свой жесткий диск.Но теперь вы спрашиваете: «Как насчет моего примера? Я использовал одно и то же значение (
-1
) для обоих назначений. Что может быть неясным в этом?»Вы правы ... за исключением того, как комитет по стандартам C ++ описал это.
Они могли бы сделать специальное исключение для вашего особого случая, но они этого не сделали. (А почему они? Что толку было бы, что когда - нибудь , возможно , есть?) Так что ,
i
все еще может быть5
. Или ваш жесткий диск может быть пустым. Таким образом, ответ на ваш вопрос:Это неопределенное поведение, потому что не определено, что это за поведение.
(Это заслуживает особого внимания, потому что многие программисты думают, что «неопределенный» означает «случайный» или «непредсказуемый». Это не так; это означает, что не определено стандартом. Поведение может быть на 100% непротиворечивым и все же быть неопределенным.)
Могло ли это быть определено поведение? Да. Это было определено? Нет. Следовательно, это "неопределено".
Тем не менее, «undefined» не означает, что компилятор отформатирует ваш жесткий диск ... это означает, что он может и все равно будет совместимым со стандартами компилятором. На самом деле, я уверен, что g ++, Clang и MSVC будут делать то, что вы ожидали. Они просто не "должны".
Другой вопрос может быть: Почему комитет по стандартам C ++ решил сделать этот побочный эффект неубедительным? , Этот ответ будет включать в себя историю и мнения комитета. Или что хорошего в том, что этот побочный эффект не секвенируется в C ++? , которая допускает любое обоснование, было ли это фактическим обоснованием комитета по стандартам. Вы можете задать эти вопросы здесь или на сайте programmers.stackexchange.com.
источник
-Wsequence-point
g ++, он предупредит вас.undefined behavior
знакомых с техническими спецификациями, считаютsomething random will happen
, что это далеко не всегда.Практическая причина не делать исключения из правил только потому, что два значения одинаковы:
Рассмотрим случай, когда это было разрешено.
Теперь, спустя несколько месяцев, возникает необходимость изменить
Кажется безобидным, не так ли? И все же внезапно prog.cpp больше не будет компилироваться. Тем не менее, мы считаем, что компиляция не должна зависеть от значения литерала.
Итог: нет никаких исключений из правила, потому что оно сделает успешную компиляцию зависимой от значения (скорее от типа) константы.
РЕДАКТИРОВАТЬ
@HeartWare указал, что константные выражения формы
A DIV B
не допускаются в некоторых языках, когдаB
это 0, и приводят к сбою компиляции. Следовательно, изменение константы может привести к ошибкам компиляции в другом месте. Что, ИМХО, неудачно. Но, конечно, хорошо ограничивать такие вещи неизбежным.источник
f(i = VALUEA, i = VALUEB);
определенно есть потенциал для неопределенного поведения. Я надеюсь, что вы на самом деле не кодируете значения за идентификаторы.SomeProcedure(A, B, B DIV (2-A))
. В любом случае, если в языке говорится, что CONST должен быть полностью оценен во время компиляции, то, конечно, моя заявка недействительна для этого случая. Так как это как-то размывает различие времени компиляции и времени выполнения. Будет ли это замечать, если мы напишемCONST C = X(2-A); FUNCTION X:INTEGER(CONST Y:INTEGER) = B/Y;
? Или функции не разрешены?Путаница заключается в том, что сохранение постоянного значения в локальной переменной - это не одна атомарная инструкция в каждой архитектуре, для которой предназначен C. В этом случае процессор, на котором работает код, имеет большее значение, чем компилятор. Например, в ARM, где каждая инструкция не может нести полную 32-битную константу, хранение int в переменной требует более одной инструкции. Пример с этим псевдокодом, где вы можете хранить только 8 битов за раз и должны работать в 32-битном регистре, я - int32:
Вы можете себе представить, что если компилятор хочет оптимизировать его, он может чередовать одну и ту же последовательность дважды, и вы не знаете, какое значение будет записано в i; и скажем что он не очень умный
Однако в моих тестах gcc достаточно любезен, чтобы признать, что одно и то же значение используется дважды и генерирует его один раз, и ничего странного не делает. Я получаю -1, -1 Но мой пример все еще действителен, так как важно учитывать, что даже константа может быть не такой очевидной, как кажется.
источник
-1
(что компилятор где-то сохранил), а скорее3^81 mod 2^32
, но постоянно, тогда компилятор может делать именно то, что сделано здесь, и в некотором рычаге омтимизации я чередую последовательности вызовов чтобы избежать ожидания.f(i = A, j = B)
гдеi
иj
два отдельных объекта. Этот пример не имеет UB. Машина, имеющая 3 коротких регистра, не является оправданием для компилятора смешивать два значенияA
иB
в одном и том же регистре (как показано в ответе @ davidf), потому что это нарушит семантику программы.Поведение обычно указывается как неопределенное, если есть какая-то возможная причина, по которой компилятор, который пытался быть «полезным», мог сделать что-то, что вызвало бы совершенно неожиданное поведение.
В случае, когда переменная записывается несколько раз, и ничто не гарантирует, что запись происходит в разное время, некоторые виды оборудования могут позволить одновременно выполнять несколько операций «хранения» по разным адресам с использованием двухпортовой памяти. Однако в некоторых двухпортовых запоминающих устройствах явно запрещен сценарий, когда два хранилища обращаются к одному и тому же адресу одновременно, независимо от того, соответствуют ли записанные значения, Если компилятор для такой машины замечает две непоследовательные попытки записать одну и ту же переменную, он может либо отказаться от компиляции, либо гарантировать, что две записи не могут быть запланированы одновременно. Но если один или оба доступа осуществляются через указатель или ссылку, компилятор не всегда может определить, могут ли обе записи достичь одного и того же места хранения. В этом случае он может запланировать запись одновременно, вызывая аппаратную ловушку при попытке доступа.
Конечно, тот факт, что кто-то может реализовать компилятор C на такой платформе, не предполагает, что такое поведение не должно определяться на аппаратных платформах при использовании хранилищ типов, достаточно маленьких, чтобы обрабатываться атомарно. Попытка сохранить два разных значения в неупорядоченной форме может привести к странностям, если компилятор не знает об этом; например, учитывая:
если компилятор вставляет вызов «moo» и может сказать, что он не изменяет «v», он может сохранить 5 в v, затем сохранить 6 в * p, затем передать 5 в «zoo», а затем передать содержимое v в "зоопарк". Если «zoo» не изменяет «v», не должно быть никакого способа, чтобы двум вызовам передавались разные значения, но это может легко произойти в любом случае. С другой стороны, в тех случаях, когда оба хранилища будут записывать одно и то же значение, такая странность не может возникнуть, и на большинстве платформ не будет разумной причины для того, чтобы реализация сделала что-то странное. К сожалению, некоторые авторы компиляторов не нуждаются в каких-либо оправданиях для глупого поведения, кроме «потому что это допускает Стандарт», поэтому даже эти случаи небезопасны.
источник
Тот факт, что в этом случае результат будет одинаковым в большинстве реализаций, является случайным; порядок оценки пока не определен. Подумайте
f(i = -1, i = -2)
: здесь порядок имеет значение. Единственная причина, по которой это не имеет значения в вашем примере - это случайность того, что оба значения-1
.Учитывая, что выражение указано как выражение с неопределенным поведением, злонамеренно совместимый компилятор может отображать неподходящее изображение при оценке
f(i = -1, i = -1)
и прерывании выполнения - и при этом считаться полностью правильным. К счастью, никаких компиляторов, о которых я знаю, не делают.источник
Мне кажется, что единственное правило, относящееся к последовательности выражения аргумента функции:
Это не определяет последовательность между выражениями аргументов, поэтому мы заканчиваем в этом случае:
На практике на большинстве компиляторов приведенный вами пример будет работать нормально (в отличие от «стирания жесткого диска» и других теоретических неопределенных последствий поведения).
Это, однако, является обязанностью, поскольку зависит от конкретного поведения компилятора, даже если два назначенных значения одинаковы. Кроме того, очевидно, что если вы попытаетесь назначить разные значения, результаты будут «действительно» неопределенными:
источник
C ++ 17 определяет более строгие правила оценки. В частности, это последовательность аргументов функции (хотя и в неопределенном порядке).
Это позволяет некоторые случаи, которые были бы UB раньше:
источник
f
подпись былаf(int a, int b)
, гарантирует ли C ++ 17 этоa == -1
иb == -2
если вызывается, как во втором случае?a
иb
, то либоi
-then-a
инициализируется -1, после чегоi
-then-b
инициализируется до -2, или наоборот. В обоих случаях мы заканчиваем сa == -1
иb == -2
. По крайней мере, так я прочитал: « Инициализация параметра, включая каждое связанное с ним вычисление значения и побочный эффект, определяется неопределенным образом относительно последовательности любого другого параметра ».Оператор присваивания может быть перегружен, и в этом случае порядок может иметь значение:
источник
Это просто ответ на вопрос «Я не уверен, что может означать« скалярный объект »помимо чего-то вроде int или float».
Я бы интерпретировал «скалярный объект» как аббревиатуру «объекта скалярного типа» или просто «переменная скалярного типа». Затем
pointer
,enum
(константа) имеют скалярного типа.Это статья о скалярных типах в MSDN .
источник
На самом деле, есть причина не зависеть от того факта, что компилятор проверит, что
i
ему присвоено одно и то же значение дважды, так что его можно заменить одним присваиванием. Что если у нас есть выражения?источник
1
наi
. Либо оба аргумента назначают,1
и это делает «правильную» вещь, либо аргументы назначают разные значения, и это неопределенное поведение, поэтому наш выбор все еще разрешен.