Страницы

Поиск по вопросам

понедельник, 15 октября 2018 г.

Оператор присваивания в C++

Есть несколько моментов в описании оператора присваивания (assignment operator) в стандарте языка, которые мне не ясны, и хотелось бы их прояснить.
n4659, 8.18/1:
[...] In all cases, the assignment is sequenced after the value computation of the right and left operands, and before the value computation of the assignment expression. The right operand is sequenced before the left operand. With respect to an indeterminately-sequenced function call, the operation of a compound assignment is a single evaluation. [Note: Therefore, a function call shall not intervene between the lvalue-to-rvalue conversion and the side effect associated with any single compound assignment operator. —end note]
Вопрос 1. Что такое value computation и выполняется ли оно с точки зрения стандарта для левого и правого операндов в следующем коде:
a = 1;
Вопрос 2. Что имеется в виду, когда говорится о неопределённой упорядоченности вызова функции? Хотелось бы увидеть пример, с участием присваивания и вызова функции в одном выражении, где вызов функции неопределённо упорядочен, относительно чего-либо.
n4659, 8.18/8:
If the value being stored in an object is read via another object that overlaps in any way the storage of the first object, then the overlap shall be exact and the two objects shall have the same type, otherwise the behavior is undefined. [Note: This restriction applies to the relationship between the left and right sides of the assignment operation; it is not a statement about how the target of the assignment may be aliased in general. See 6.10. —end note]
Вопрос 3. Хотелось бы увидеть пример, который попадает под описание приведённой выше цитаты.

Дополнение к вопросу 1.
Наверное, больший смысл имеет поставить вопрос следующим образом: всегда ли с точки зрения стандарта языка выполняется value computation для операндов оператора присваивания? И если нет, то когда выполняется, а когда нет?
Дело то вот в чём. В первой цитате в этом вопросе говорится, что присваивание происходит после того, как произойдёт value computation обоих операндов. А ещё говорится, что правый операнд упорядочен перед левым (т.е. value computations + side effects, ассоциированные с правым операндом, произойдут до value computations + side effects ассоциированных с левым операндом).
Таким образом, если в операторе присваивания всегда происходит value computation левого операнда перед собственно присвоением, то правый операнд всегда упорядочен перед присвоением.


Ответ

«value computation» это процесс вычисления значения выражения, т.е. получение его результата. К примеру, пусть у нас есть int i = 0, тогда вычисленное выражение i++ вернёт 0. Тут важен тот факт, что i++ содержит побочные эффекты (side effects). Так вот, имея вычисленное вышеозначенное выражение, мы не гарантируем, что к нему в то же время были применены требуемые изменения (инкремент). Почему это может быть важно? Возьмём для этого стандарт до 17 года. В нём выражение: i = i++ имеет неопределённое поведение, потому что инкремент для i никак не упорядочен по отношению к присваиванию: он может произойти до или же после.
С другой стороны, если мы возьмём выражение ++i, то в нём побочные эффекты применяются до вычисления выражения, что делает такое выражение i = ++i рабочим и не содержащим неопределённого поведения.

Теперь ко второму вопросу. Давайте предположим у нас есть глобальная переменная: long long global = 0; и функция auto foo() {return global += 1;}. Для примера нам так же понадобится такая функция void ignore(long long, long long) и C++17 (исключительно ради гарантии неопределённой упорядоченности (indeterminately sequenced) вычисления аргументов функции).
Наконец, рассмотрим такой код: ignore(global += BIG_NUMBER, foo()), так вот, в теории, если убрать это правило, то могла бы получится следующая ситуация (на какой-нибудь платформе, где запись long long будет в несколько инструкций): мы начинаем записывать в наш global извне, записываем часть, но тут происходит переключение на код foo (так решил компилятор) и переписывает то, что мы записали. После чего мы продолжаем запись и в нашей переменной global лежит мусор. Т.е. это правило запрещает перемежение (interleaving, overlapping) вычисления выражений, и компилятор не имеет права сделать то, что я описал в предыдущем предложении.
Но это всё теория, потому что стандарт явно запрещает (C++14[intro.object]p13) перемежение всех выражений, за исключением неупорядоченных (unsequenced), поэтому подобные уточнения мне кажутся лишними.

Третья цитата запрещает такое:
int i = 0; char* ptr = reinterpret_cast(&i); // Вот тут проблема *ptr = static_cast(i);
Т.е. слева и справа не должно быть двух объектов, которые указывают на одну область памяти, но имеют разные типы.

По поводу дополнения: тут просто стоит привести цитату из C++17[intro.object]p15:
<...> An expression X is said to be sequenced before an expression Y if every value computation and every side effect associated with the expression X is sequenced before every value computation and every side effect associated with the expression Y <...>
И этого будет достаточно. Согласно правилам, введённым в C++17, правая часть выражения присваивания упорядочена до (sequenced before):
The right operand is sequenced before the left operand
Это является ключом к пониманию: прежде чем мы присвоим новое значение, или даже начнём вычислять то, чему мы там присваиваем, всё, что стоит справа должно отработать. Это правило делает код, i = i++ правильным, хотя до C++17 это было неопределённым поведением.

Комментариев нет:

Отправить комментарий