C ++ и PHP против 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.

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

30

Решение

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

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

§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. Составные операторы присваивания спецификации Java также говорят, что E1 op= E2 эквивалентно E1 = (T) ((E1) op (E2)),

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

Так что у вашего компилятора Java нет ошибки. Это просто соответствует спецификациям.

48

Другие решения

Благодаря комментариям 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);
}
}

должен print 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.

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

21

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

Так почему же 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) имеет + как почти любой другой метод, но оптимизация компилятором JIT может быть результат в другом порядке оценки. С другой стороны, так как C # (и .NET) делает определить порядок оценки / выполнения, так что я думаю, что совместимый компилятор должен всегда произвести этот результат.

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

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

РЕДАКТИРОВАТЬ:

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

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

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

7

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

JLS 15.26.2, Составные операторы присваивания

Составное выражение присваивания формы
E1 op= E2
эквивалентно
E1
= (T) ((E1) op (E2))

, где
T
это тип
E1
, Кроме этого
E1
оценивается
только однажды.

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

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 сохраняет результат оценки каждой стороны (сначала слева), а затем оценивает результат выполнения оператора для сохраненных значений.

4
По вопросам рекламы [email protected]