Страницы

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

вторник, 10 декабря 2019 г.

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

#cpp #language_lawyer


Есть несколько моментов в описании оператора присваивания (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
левого операнда перед собственно присвоением, то правый операнд всегда упорядочен перед
присвоением. 
    


Ответы

Ответ 1



«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 это было неопределённым поведением.

Ответ 2



Как слева, так и справа от оператора присваивания могут быть более сложные сущности, нежели указаны в вашем примере. Например, слева может быть функция, которая возвращает ссылку, а справа функция возвращающее значение. Вот вызов этих функций для получения значения правой и левой части и есть value computation. Для вашего случая можно сказать, что слева вычисляется адрес переменной a, а справа значение литерала 1. Фраза про "indeterminately-sequenced function call" говорит о том, что составные операторы типа +=, *= и т.д. стоит рассматривать как одиночное (неделимое) вычисление при использовании в качестве аргумента функции. Например, при вызове следующей функции: f (a() += b(), c()) вызов c() не может произойти между вызовами a() и b(). По третьему вопросу - представьте, что у вас есть массив байтов. И вы имеете два указателя более тяжелого типа нежели байт, на данные из этого массива. При этом разыменование этих указателей будет использовать некоторую общую часть исходного массива, то есть иметь пересечение. Вот такие разыменованные указатели и нельзя использовать в присваивании.

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

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