C ++ и PHP vs C # и Java – неравные результаты

Я нашел что-то странное в C # и Java. Давайте посмотрим на этот код на C ++:

#include <iostream> using namespace std; class Simple { public: static int f() { X = X + 10; return 1; } static int X; }; int Simple::X = 0; int main() { Simple::X += Simple::f(); printf("X = %d", Simple::X); return 0; } 

В консоли вы увидите X = 11 ( Посмотрите на результат здесь – IdeOne C ++ ).

Теперь давайте посмотрим на тот же код на C #:

 class Program { static int x = 0; static int f() { x = x + 10; return 1; } public static void Main() { x += f(); System.Console.WriteLine(x); } } 

В консоли вы увидите 1 (не 11!) (Посмотрите на результат здесь – IdeOne C # Я знаю, что вы сейчас думаете – «Как это возможно?», Но давайте перейдем к следующему коду.

Код Java:

 import java.util.*; import java.lang.*; import java.io.*; /* Name of the class has to be "Main" only if the class is public. */ class Ideone { static int X = 0; static int f() { X = X + 10; return 1; } public static void main (String[] args) throws java.lang.Exception { Formatter f = new Formatter(); f.format("X = %d", X += f()); System.out.println(f.toString()); } } 

Результат такой же, как в C # (X = 1, посмотрите на результат здесь ).

И в последний раз давайте посмотрим на PHP-код:

 <?php class Simple { public static $X = 0; public static function f() { self::$X = self::$X + 10; return 1; } } $simple = new Simple(); echo "X = " . $simple::$X += $simple::f(); ?> 

Результат – 11 (посмотрите здесь результат).

У меня есть небольшая теория – эти языки (C # и Java) создают локальную копию статической переменной X в стеке (игнорируют ли они статическое ключевое слово?). И это причина, по которой результатом этих языков является 1.

Кто-нибудь здесь, у кого есть другие версии?

Стандарт C ++ гласит:

Что касается вызова функции с неопределенной последовательностью, то операция составного присвоения представляет собой единую оценку. [Примечание: поэтому вызов функции не должен вмешиваться между преобразованием lvalue-to-rval и побочным эффектом, связанным с любым единственным оператором присваивания. -end note]

§5.17 [expr.ass]

Следовательно, как и в той же оценке, вы используете X и функцию с побочным эффектом на X , результат не определен, потому что:

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

§1.9 [intro.execution]

На многих компиляторах 11, но нет никакой гарантии, что компилятор C ++ не даст вам 1, как для других языков.

Если вы все еще настроены скептически, другой анализ стандарта приводит к такому же выводу: стандарт также говорит в том же разделе, что и выше:

Поведение выражения вида E1 op = E2 эквивалентно E1 = E1 op E2 за исключением того, что E1 оценивается только один раз.

В вашем случае X = X + f() за исключением того, что X оценивается только один раз.
Поскольку нет гарантии по порядку оценки, в X + f() вы не можете считать само собой разумеющимся, что первый f оценивается, а затем X

Добавление: я не эксперт по Java, но в правилах java четко указывается порядок оценки в выражении, который гарантированно будет слева направо в разделе 15.7 спецификаций java . В разделе 15.26.2. Compound Assignment Operators, java specs также говорят, что E1 op= E2 эквивалентен E1 = (T) ((E1) op (E2)) .

В вашей программе java это снова означает, что ваше выражение эквивалентно X = X + f() и сначала оценивается X , затем f() . Таким образом, побочный эффект f() не учитывается в результате.

Поэтому у вашего java-компилятора нет ошибки. Он просто соответствует спецификациям.

Благодаря комментариям Deduplicator и user694733, здесь приведена измененная версия моего первоначального ответа.


Версия C ++ имеет не определено неуказанное поведение.

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

За исключением очень редких случаев, вы всегда захотите избежать обоих.


Хорошей отправной точкой для понимания всей проблемы являются часто задаваемые вопросы по C ++. Почему некоторые люди думают, что x = ++ y + y ++ – это плохо? , Какова ценность i ++ + i ++? и Какова сделка с «точками последовательности»? :

Между предыдущей и следующей точкой последовательности скалярный объект должен иметь значение , которое его хранимое значение изменялось не более одного раза путем оценки выражения.

(…)

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

(…)

В определенных определенных точках последовательности выполнения, называемых точками последовательности, все побочные эффекты предыдущих оценок должны быть полными и никаких побочных эффектов последующих оценок не должно быть. (…) «Определенные определенные точки», которые называются точками последовательности, являются (…) после оценки всех параметров функции, но до того, как выполняется первое выражение внутри функции.

Короче говоря, изменение переменной дважды между двумя последовательными точками последовательности дает неопределенное поведение, но вызов функции вводит промежуточную точку последовательности (фактически, две промежуточные точки последовательности, потому что оператор return создает другой).

Это означает, что у вас есть вызов функции в вашем выражении «сохраняет» ваш Simple::X += Simple::f(); от неопределенности и превращает ее в «только» неуказанную.

И 1, и 11 – возможные и правильные результаты, тогда как печать 123, сбой или отправка оскорбительного сообщения электронной почты вашему боссу не допускаются к поведению; вы никогда не получите гарантии, будет ли напечатан 1 или 11.


Следующий пример немного отличается. Это, по-видимому, упрощение исходного кода, но действительно помогает выделить разницу между неопределенным и неуказанным поведением:

 #include <iostream> int main() { int x = 0; x += (x += 10, 1); std::cout << x << "\n"; } 

Здесь поведение действительно не определено, потому что вызов функции ушел, поэтому обе модификации x встречаются между двумя последовательными точками последовательности. Компилятор разрешен спецификацией языка C ++ для создания программы, которая печатает 123, сбой или посылает оскорбительное письмо вашему боссу.

(Конечно, электронная почта – это очень распространенная юмористическая попытка объяснить, как неопределенное действительно означает что-либо . Сбои часто являются более реалистичным результатом неопределенного поведения.)

Фактически, , 1 (как и оператор возврата в вашем исходном коде) – это красная селедка. Следующее приводит к неопределенному поведению:

 #include <iostream> int main() { int x = 0; x += (x += 10); std::cout << x << "\n"; } 

Это может печатать 20 (это делается на моей машине с VC ++ 2013), но поведение по-прежнему не определено.

(Примечание: это относится к встроенным операторам. Перегрузка оператора изменяет поведение обратно на указанное , поскольку перегруженные операторы копируют синтаксис из встроенных, но имеют семантику функций, а это означает, что перегруженный оператор += пользовательского тип, который появляется в выражении, фактически является вызовом функции . Следовательно, не только введенные точки последовательности, но и вся неопределенность уходят, выражение становится эквивалентным x.operator+=(x.operator+=(10)); порядок оценки аргументов. Это, вероятно, не имеет отношения к вашему вопросу, но все равно следует упомянуть).

Напротив, версия Java

 import java.io.*; class Ideone { public static void main(String[] args) { int x = 0; x += (x += 10); System.out.println(x); } } 

должен печатать 10. Это связано с тем, что Java не имеет ни неопределенного, ни неуказанного поведения в отношении порядка оценки. О точках последовательности не нужно беспокоиться. См. Спецификацию языка Java 15.7. Порядок оценки :

Язык программирования Java гарантирует, что операнды операторов, по-видимому, оцениваются в определенном порядке оценки, а именно слева направо.

Таким образом, в случае Java x += (x += 10) , интерпретируемый слева направо, означает, что сначала что-то добавляется к 0 , а что-то 0 + 10 . Следовательно, 0 + (0 + 10) = 10 .

См. Также пример 15.7.1-2 в спецификации Java.

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


Честно говоря, я не знаю о C # и PHP, но я бы предположил, что оба они имеют некоторый гарантированный порядок оценки. C ++, в отличие от большинства других языков программирования (но, как и C), имеет тенденцию допускать гораздо более неопределенное и неопределенное поведение, чем другие языки. Это не хорошо или плохо. Это компромисс между надежностью и эффективностью . Выбор правильного языка программирования для конкретной задачи или проекта всегда является вопросом анализа компромиссов.

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

Последнее слово:

Я нашел небольшую ошибку на C # и Java.

Вы не должны предполагать, что найдете ошибки в спецификациях языка или компиляторах, если у вас нет многолетнего профессионального опыта в качестве инженера-программиста.

Как уже писал Кристоф, это в основном неопределенная операция.

Итак, почему C ++ и PHP делают это в одну сторону, а C # и Java – наоборот?

В этом случае (который может быть другим для разных компиляторов и платформ) порядок оценки аргументов в C ++ инвертирован по сравнению с C # -C #, оценивает аргументы в порядке написания, тогда как образец C ++ делает это наоборот. Это сводится к соглашениям об использовании по умолчанию, которые используются, но опять же – для C ++ это неопределенная операция, поэтому она может отличаться в зависимости от других условий.

Чтобы проиллюстрировать этот код C #:

 class Program { static int x = 0; static int f() { x = x + 10; return 1; } public static void Main() { x = f() + x; System.Console.WriteLine(x); } } 

Будет производить 11 на выходе, а не 1 .

Это просто потому, что C # оценивает «по порядку», поэтому в вашем примере он сначала считывает x а затем вызывает f() , а в моем – сначала вызывает f() а затем читает x .

Теперь это все еще может быть нереалистичным. IL (.NET bytecode) имеет + как практически любой другой метод, но оптимизация компилятором JIT может привести к другому порядку оценки. С другой стороны, поскольку C # (и .NET) определяет порядок оценки / выполнения, поэтому я думаю, что совместимый компилятор должен всегда давать этот результат.

В любом случае, это прекрасный неожиданный результат, который вы нашли, и предостерегающая история – побочные эффекты в методах могут быть проблемой даже на императивных языках 🙂

О, и, конечно же, static означает что-то другое в C # или C ++. Я видел ошибку, допущенную C ++ ers, которая раньше приходила на C #.

EDIT :

Позвольте мне немного расширить проблему «разных языков». Вы автоматически предположили, что результат C ++ является правильным, потому что когда вы делаете расчет вручную, вы делаете оценку в определенном порядке – и вы определили, что этот порядок соответствует результатам C ++. Однако ни C ++, ни C # не анализируют выражение – это просто куча операций над некоторыми значениями.

C ++ хранит x в регистре, как и C #. Просто C # сохраняет его перед оценкой вызова метода, а C ++ – после него . Если вы измените код на C ++, чтобы вместо этого сделать x = f() + x , как и в C #, я ожидаю, что вы получите 1 на выходе.

Наиболее важная часть состоит в том, что C ++ (и C) просто не указали явный порядок операций, вероятно, потому, что он хотел использовать архитектуры и платформы, которые выполняют либо один из этих заказов. Поскольку C # и Java были разработаны в то время, когда это уже не имеет никакого значения, и поскольку они могут учиться на всех этих ошибках C / C ++, они указали явный порядок оценки.

Согласно спецификации языка Java:

JLS 15.26.2, Операторы назначения контировки

Составляющее выражение присваивания формы E1 op= E2 эквивалентно E1 = (T) ((E1) op (E2)) , где T – тип E1 , за исключением того, что E 1 оценивается только один раз.

Эта небольшая программа демонстрирует разницу и демонстрирует ожидаемое поведение на основе этого стандарта.

 public class Start { int X = 0; int f() { X = X + 10; return 1; } public static void main (String[] args) throws java.lang.Exception { Start actualStart = new Start(); Start expectedStart = new Start(); int actual = actualStart.X += actualStart.f(); int expected = (int)(expectedStart.X + expectedStart.f()); int diff = (int)(expectedStart.f() + expectedStart.X); System.out.println(actual == expected); System.out.println(actual == diff); } } 

В порядке,

  1. actual присваивается значение actualStart.X += actualStart.f() .
  2. expected присваивается значению
  3. результат извлечения actualStart.X , который равен 0 , и
  4. применение оператора добавления к actualStart.X с
  5. возвращаемое значение вызова actualStart.f() , которое равно 1
  6. и присваивая результат 0 + 1 expected .

Я также объявил diff чтобы показать, как изменение порядка вызова изменяет результат.

  1. diff присваивается значение
  2. возвращаемое значение вызова diffStart.f() , с равен 1 , и
  3. применяя оператор сложения к этому значению с помощью
  4. значение diffStart.X (которое равно 10, побочный эффект diffStart.f()
  5. и присваивая результат 1 + 10 diff .

В Java это не неопределенное поведение.

Редактировать:

Чтобы обратиться к вашей точке относительно локальных копий переменных. Это правильно, но это не имеет ничего общего со static . Java сохраняет результат оценки каждой стороны (сначала слева), затем оценивает результат выполнения оператора по сохраненным значениям.