Как определить, что «предшествует» другим?

Я просмотрел этот отличный ответ относительно отношений Undefined Behavior и Sequenced [Before/After] в C ++11. Я понимаю концепции бинарных отношений, но не понимаю новых правил, регулирующих последовательность.

Как применяются новые правила последовательности для этих знакомых примеров?

  1. i = ++i;
  2. a[++i] = i;

В частности, каковы новые правила последовательности C++11?

Я ищу некоторые правила, такие как (это полностью выдумано)

lhs оператора '=' всегда располагается перед rhs и, таким образом, оценивается первым.

Если они доступны в самом стандарте, может ли кто-нибудь процитировать их здесь?


person Lazer    schedule 05.03.2012    source источник
comment
Предполагается, что в правилах точек следования не должно быть никаких изменений, поскольку старый код должен работать так же, как и раньше. Он просто перефразирован, чтобы справиться с потоками.   -  person Bo Persson    schedule 05.03.2012
comment
@BoPersson: но новые правила могут быть (и являются) более точными. Вещи, которые раньше были UB, могут быть четко определены в C++11 без нарушения старого кода. :)   -  person jalf    schedule 08.03.2012


Ответы (2)


Отношение sequenced-before и связанные с ним правила представляют собой упорядочение предшествующих правил для точек следования, определенных согласованным образом с другими отношениями модели памяти, такими как происходит-before< /em> и синхронизируется с, чтобы можно было точно указать, какие операции и эффекты видны и при каких обстоятельствах.

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

Начнем с ваших примеров:

1. i = ++i;

Если i является встроенным типом, таким как int, то вызовы функций не задействованы, все является встроенным оператором. Таким образом, происходят 4 вещи:

(a) вычисление значения ++i, которое равно исходному-значению-i +1

(b) побочный эффект ++i, который сохраняет исходное-значение-i +1 обратно в i

(c) вычисление значения присвоения, которое представляет собой просто сохраненное значение, в данном случае результат вычисления значения ++i

(d) побочный эффект присваивания, который сохраняет новое значение в i

Все эти вещи последовательны-перед следующим полным выражением. (т.е. все они завершаются последней точкой с запятой оператора)

Поскольку ++i эквивалентно i+=1, побочным эффектом сохранения значения является упорядочивание вычисления значения ++i, поэтому (b) последовательно-до (а).

вычисление значения обоих операндов присваивания выполняется в последовательности перед вычислением значения самого присваивания, а это, в свою очередь, в последовательности перед побочным эффектом сохранения значения. Следовательно, (a) последовательно расположено перед (c), а (c) последовательно-перед (d).

Таким образом, у нас есть (b) -> (a) -> (c) -> (d), и, таким образом, это нормально по новым правилам, тогда как в C++98 это было неправильно.

Если бы i было class, тогда выражение было бы i.operator=(i.operator++()) или i.operator=(operator++(i)), а все эффекты вызова operator++ последовательны перед вызовом operator=.

2. a[++i] = i;

Если a — это тип массива, а i — это int, то выражение снова состоит из нескольких частей:

(a) вычисление значения i

(b) вычисление значения ++i

(c) побочный эффект ++i, который сохраняет новое значение обратно в i

(d) вычисление значения для a[++i], которое возвращает lvalue для элемента a, индексированного вычислением значения для ++i.

(e) вычисление значения присваивания, которое представляет собой просто сохраненное значение, в данном случае результат вычисления значения i

(f) побочный эффект присваивания, при котором новое значение сохраняется в элементе массива a[++i].

Опять же, все эти вещи последовательны-перед следующим полным выражением. (т.е. все они завершаются последней точкой с запятой оператора)

Опять же, поскольку ++i эквивалентно i+=1, побочным эффектом сохранения значения является упорядочивание до вычисления значения ++i, поэтому (c ) находится в последовательности-до (b).

вычисление значения индекса массива ++i выполняется *sequenced-before` вычисление значения выбора элемента, поэтому (b) выполняется sequence-before (г).

вычисление значения обоих операндов присваивания выполняется в последовательности перед вычислением значения самого присваивания, а это, в свою очередь, в последовательности перед побочным эффектом сохранения значения. Следовательно, (a) и (d) последовательны до (e), а (e) последовательны до (f).

Таким образом, мы имеем две последовательности: (a) -> (d) -> (e) -> (f) и (c) -> (b) -> (d) -> (e) -> (f).

К сожалению, порядок между (а) и (с) отсутствует. Таким образом, побочный эффект, который сохраняется в i, является неупорядоченным по отношению к вычислению значения на i, и код демонстрирует неопределенное поведение< /сильный>. Это снова дается 1.9p15 стандарта C++11.

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

Правила

Правила относительно просты:

  1. Вычисление значения аргументов встроенного оператора выполняется в последовательности перед вычислением значения самого оператора.

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

  3. вычисление значения любого другого встроенного оператора выполняется в последовательности перед побочными эффектами этого оператора.

  4. вычисление значения и побочные эффекты левой части встроенного оператора запятой упорядочиваются-перед вычисление значения и побочные эффекты правой части.

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

  6. вычисление значения и побочные эффекты аргументов вызова функции упорядочиваются перед первым полным выражением в функции.

  7. вычисление значения и побочные эффекты всего, внутри функции, упорядочиваются — до вычисления значения< /em> результата.

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

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

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

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

person Anthony Williams    schedule 06.03.2012
comment
Я думаю, что вы ошиблись в первом случае. ++i эквивалентен (i=i+1), и вычисление значения присваивания выполняется после побочного эффекта. Во всех случаях присваивание выполняется после вычисления значения правого и левого операндов и перед вычислением значения выражения присваивания. (раздел 5.17 [expr.ass] в моем pdf), поэтому две операции записи упорядочены, и i=++i действителен. Это также означает, что новые правила отличаются от старых даже в случае одного потока (i=++i — это UB в старом C++). - person 6502; 07.03.2012
comment
Вы правы, что ++i эквивалентно i+=1. (5.3.2). Я не уверен, что ваш вывод следует. Мне придется перепроверить. - person Anthony Williams; 07.03.2012

На мой взгляд, это гораздо более сложное правило, чем старое правило точек последовательности, и я не уверен на 100%, что понял его правильно ... в любом случае IIUC все сводится к тому, если для получения значения вам нужен побочный эффект быть уже применены.

Первый случай

i = ++i;

Здесь, чтобы выполнить присваивание, вам нужно значение правой части, и чтобы получить это значение, вам нужно, чтобы побочный эффект уже был применен; поэтому здесь назначение упорядочено после приращения, и все в порядке. Важным моментом здесь является то, что для выполнения задания вам нужно значение RHS и только адрес LHS.

Резюме:

  1. назначение упорядочено после &i и ++i
  2. ++i располагается после приращения
  3. (транзитивность) присваивание выполняется после приращения

Значение i читается только один раз после приращения. Он записывается дважды, один раз приращением и один раз присваиванием, но эти две операции выполняются последовательно (сначала приращение, затем присваивание).

Второй случай

a[++i] = i;

Здесь вместо этого вам нужно значение i для RHS и значение ++i для LHS. Однако эти два выражения не упорядочиваются (оператор присваивания не навязывает последовательность), и поэтому результат не определен.

Резюме:

  1. назначение упорядочено после &a[++i] и i
  2. &a[++i] следует за ++i
  3. ++i располагается после приращения

Здесь значение i считывается дважды, один раз для левой стороны присвоения и один раз для правой. Часть LHS также выполняет модификацию (приращение). Этот доступ для записи и доступ для чтения назначения RHS, однако, не упорядочены по отношению друг к другу, и поэтому это выражение равно UB.

Заключительный разглагольствование

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

В некотором смысле новые правила не нанесли ущерба программам на C++ (UB — враг, и теперь в этой области меньше UB), но нанесли ущерб языку, увеличив сложность (и наверняка что-то, в чем С++ не нуждался, было добавлено сложность).

Также обратите внимание, что забавная вещь в ++i заключается в том, что возвращаемое значение является l-значением (поэтому ++ ++ i допустимо), так что это в основном адрес, и логически не было необходимости, чтобы возвращаемое значение упорядочено после приращения. Но так сказано в стандарте, и это правило вам нужно вбить в свои нейроны. Конечно, чтобы иметь «полезный» ++i, вы хотите, чтобы пользователи значения получали обновленное значение, но все же, поскольку оператор ++ видит вещи (он возвращает адрес, на который не влияет приращение), эта последовательность не была формально необходим.

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

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

person 6502    schedule 05.03.2012
comment
Ух ты. Я не знал, что такое изменение правил вообще произошло. Я согласен с вашей оценкой. Я хочу, чтобы тот, кто ответил на ваш ответ, назвал причину. - person Omnifarious; 05.03.2012
comment
Возможно, требовались более строгие правила для «до/после» по отношению к отношениям «синхронизация с», необходимым для многопоточности. Учтите, что если левая/правая часть уравнения является атомарной, порядок становится более важным. - person edA-qa mort-ora-y; 05.03.2012
comment
@Omnifarious: я привык к фанатикам C++, которые просто начинают кричать как сумасшедшие, как только вы говорите, что C++ не самый совершенный язык в мире. Мне нравится C++, но я думаю, что сложность была одной из больших проблем C++. Вместо этого комитет решил, что проблема была в скорости (!), поэтому теперь у нас есть гораздо более сложные правила, например, о ссылках на r-значение (x&&) и реализации конструктора копирования и оператора присваивания по умолчанию. - person 6502; 05.03.2012
comment
@ 6502: Я согласен с тем, что C ++ сложен, однако я не согласен с фокусировкой только на скорости. Семантика перемещения — это семантика. Их введение позволяет устранить подверженные ошибкам auto_ptr и заменить их гораздо лучшими unique_ptr. Аналогичным образом введение потоков позволяет стандартизировать существующую практику в сторону лучшей переносимости. Скорость является приоритетом C++ и сильно влияет на каждое решение, но это не единственный приоритет. Что касается точек следования и последовательности до/после; новые правила могут быть более сложными, но я также склонен думать, что они более интуитивны. Время покажет. - person Matthieu M.; 05.03.2012
comment
@ 6502 - Я думаю, что сложность - это проблема, но комитет по стандартам не может ничего с ней поделать. Если, конечно, не добавить к проблеме. Тем не менее, я думаю, что изучение области дизайна того, как сделать C++ быстрее, очень полезно. Я продолжаю надеяться, что кто-нибудь завершит все эти уроки красивым, блестящим, новым языком, в котором нет автоматической сборки мусора или других недостающих «функций», а вместо этого он пытается быть тем, чем является C++, но оптимизированным и элегантным. - person Omnifarious; 05.03.2012
comment
@Omnifarious: если честно, я был бы разочарован, если бы был создан новый язык без сборки мусора. Я понимаю, что бедность и подлость — это проблема, но сборка мусора — краеугольный камень современных языков. С другой стороны, типы указателей Rust довольно интересны: встроенный эквивалент unique_ptr в языке помогает гарантировать, что некоторые переменные вообще не будут нуждаться в сборке мусора. - person Matthieu M.; 05.03.2012
comment
@MatthieuM.: Я недостаточно знаю С++ 11, чтобы понять причины такого выбора. Я где-то читал, что основной целью ссылок на rvalue была конструкция перемещения, необходимая для скорости. Если это так, то я думаю, что это плохой ход (если). Конечно, я согласен с тем, что auto_ptr была плохой идеей, и я никогда не использовал ее, несмотря на первоначальное финансирование некоторыми гуру. Я также согласен, что время покажет... мой простой ум (на данный момент) надеется, что С++ 11 не будет успешным: я боюсь, что С++ 11 требует даже больше часов отладки на строку, чем и без того большое количество С++. - person 6502; 05.03.2012
comment
Извините, но я все еще не понимаю, конкретно эту часть - Здесь, чтобы выполнить задание, вам нужно значение правильной части, и чтобы получить это значение, вам нужно, чтобы побочный эффект был уже применен, поэтому здесь задание последовательно после приращения ... Почему = навязывает последовательность в первом примере, но не во втором? Спасибо! - person Lazer; 05.03.2012
comment
@Lazer: вам нужен адрес левого выражения и значение правого выражения, прежде чем вы сможете выполнить назначение. В первом случае i = ++i адрес i известен, а ++i нужно оценить перед присвоением (поскольку вам нужно значение), поэтому все в порядке, так как ++i нужно оценить до присвоения результата этого выражения. Порядок четко определен. Во втором случае оба выражения a[++i] и i должны быть оценены до присваивания, но оба выражения перекрываются. В зависимости от того, что вы вычисляете первым, присваиваемое значение изменяется. - person Matthieu M.; 05.03.2012
comment
@ 6502: О, скорость - важный фактор в C ++, но я не думаю, что это был основной фактор для семантики перемещения. Это интересное последствие, но у нас уже была аналогичная скорость с удалением копии, и мы уже могли много использовать swap (для ручных версий код был похож на сегодняшние конструкторы перемещения). - person Matthieu M.; 05.03.2012
comment
IMO имеет смысл забыть о новых правилах и просто придерживаться старых добрых. Не следует одновременно читать и изменять ячейку памяти в одном и том же полном выражении. Это делает код намного чище, и вам не нужно беспокоиться о UB. - person JohannesD; 05.03.2012
comment
@Lazer: = не навязывает последовательность между левой и правой частью, но само назначение упорядочивается после них. Однако значение правой части упорядочено после побочного эффекта приращения (поскольку стандарт говорит об этом явно, несмотря на то, что значение действительно является адресом, на который не влияет приращение), поэтому из-за транзитивности вы получаете, что назначение упорядочено после приращения и все в порядке. Если вы думаете, что это слишком сложно, я согласен... - person 6502; 05.03.2012
comment
@JohannesD: Это хорошая идея, но, к сожалению, этого недостаточно: когда вам нужно отладить программу, вы хотите сначала понять, в чем проблема. Если тот, кто написал код, не следовал старым упрощенным правилам, вам все равно придется думать с точки зрения последовательности до/после, чтобы понять, является ли фрагмент кода проблематичным или нет. Как и в случае с auto_ptr, не использовать это хорошо, но недостаточно, потому что вам может понадобиться понять, почему программа (не написанная вами) использует это и ведет себя странно. Притворство, что auto_ptr не существует, работает для написания новых программ, но не для отладки. - person 6502; 05.03.2012
comment
@MatthieuM. - Я категорически отказываюсь использовать любой язык со сборкой мусора, когда мне важна производительность. Я использую Python, и он собирает мусор, но я не использую его, когда мне важна производительность. Различные типы интеллектуальных указателей в C++11 представляют собой своего рода сборку мусора, но я могу выбирать, когда и где именно их применять, и в этом вся разница. - person Omnifarious; 05.03.2012
comment
@Omnifarious: Ну, Rust немного раздвигает границы с помощью трех типов указателей. Один тип просто заставляет перемещать семантику на вас (один владелец в любое время), в то время как 2 других собирают мусор (один в одном потоке, а другой совместно используется несколькими потоками). Это означает, что вы выбираете, использовать ли сборку мусора или нет, поэтому важные части можно реализовать без нее. - person Matthieu M.; 05.03.2012
comment
@MatthieuM. - Ну тогда надо посмотреть. Моя главная жалоба заключается в том, что когда язык становится сборщиком мусора, он обычно теряет способность больших типов включать экземпляры меньших непримитивных типов по значению или объявлять непримитивные типы как локальные переменные, значения которых хранятся в стеке. Это разрушает локальность ссылки. Он также создает искусственное разделение между «примитивными» типами и определяемыми пользователем типами, хотя есть языки, которые этого не делают, и в результате оказываются крайне неэффективными. - person Omnifarious; 05.03.2012
comment
@Omnifarious: о да, я определенно согласен с тем, что упаковка примитивных типов определенно является проблемой для кода, критичного для производительности. Распределение стека, к сожалению, немного сложнее решить, так как вам нужен анализ побега, который, к сожалению, нетривиален. - person Matthieu M.; 05.03.2012
comment
Возможно, новые правила кажутся более сложными для вас, потому что старые правила никогда не были достаточно точными. Они просто упустили детали. Это похоже на описание мира, созданного Богом, и все становится очень легко. - person Johannes Schaub - litb; 06.03.2012
comment
@JohannesSchaub-litb: Я не уверен, что понимаю вашу точку зрения ... если старые правила не охватывают все случаи и в некоторых случаях они оставляют поведение неопределенным ... разве поведение не определено в этих случаях? Или вы имеете в виду, что они делают неразрешимыми, если поведение определено? Или неразрешимым, если разрешимым, если поведение определено? Или неразрешимым, если неразрешимым, если неразрешимым, если beh**‹ошибка переполнения стека›** - person 6502; 06.03.2012
comment
@AnthonyWilliams: Нет. i=++i Я думаю, что это твой ответ неверен. 5.17.1(N3126=10-0116) говорится, что вычисление значения самого выражения присваивания выполняется после присваивания. ++i эквивалентно (i+=1) (5.3.2), поэтому вычисление его значения выполняется после приращения. Таким образом, основное присваивание в i=++i упорядочено по транзитивности после приращения присваивания, и выражение является допустимым. Сам факт того, что кто-то, утверждающий, что вы разбираетесь в C++, может ошибаться, также подтверждает (хотя и анекдотично), что мои разглагольствования о сложности не совсем глупы. - person 6502; 08.03.2012
comment
@6502: Вы правы. Это было преднамеренным изменением в соответствии с DR, на который указал Йоханнес. Было глупо с моей стороны пропустить это. - person Anthony Williams; 08.03.2012
comment
@ 6502: На мой взгляд, C++ стал более полным и удобным для использования с добавлением ссылок rvalue и семантики перемещения. Раньше было много случаев, когда вам приходилось копировать, даже если вы не хотели или не могли: вы не можете копировать объект fstream и не хотите копировать объект map<string, big>, но их копирование было единственным способ вернуть их из функции. Отсюда трюк с объявлением объекта, инициализированного по умолчанию, и передачей его в качестве ссылки lvalue, что отстой. Теперь, с семантикой перемещения, эти старые времена ушли в прошлое. - person musiphil; 19.05.2012
comment
@ 6502 Final Rant: Новые правила последовательности более точны, чем старые правила, хотя они могут показаться немного более сложными. (Они могут показаться более интуитивными, как только вы поймете концепцию отношений частичного порядка.) Раньше последовательность определялась только с точки зрения точек последовательности на одномерной линии, и были случаи, когда вам приходилось либо накладывать чрезмерную последовательность, либо баллы или получить UB; например вы не можете представить отношение заказа a<b<e && c<d<e. Теперь можно, а новые правила позволяют более тонко контролировать. Вот почему некоторые выражения, которые ранее вызывали UB, теперь могут иметь четко определенное поведение. - person musiphil; 19.05.2012