Подтвердить что ты не робот

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

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

#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 С++).

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

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 С# Я знаю, что вы сейчас думаете: "Как это возможно?", Но отпустите следующий код.

Код 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());
    }
}

Результат будет таким же, как в С# (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 (посмотрите на результат здесь).

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

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

4b9b3361

Ответ 1

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

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

§5.17 [expr.ass]

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

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

§1.9 [intro.execution]

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

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

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

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

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

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

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

Ответ 2

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


Версия С++ имеет неопределенное поведение undefined.

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

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


Хорошей отправной точкой для понимания всей проблемы являются часто задаваемые вопросы С++ Почему некоторые люди думают, что x = ++ y + y ++ - это плохо?, Что такое я ++ + я ++? и Какая сделка с "точками последовательности" ?:

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

(...)

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

(...)

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

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

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

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


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

#include <iostream>

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

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

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

Фактически, , 1 (как и оператор return в вашем исходном коде) - это красная селедка. Ниже приведено поведение undefined:

#include <iostream>

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

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

(Примечание: это относится к встроенным операторам. Перегрузка оператора изменяет поведение обратно на указанное, поскольку перегруженные операторы копируют синтаксис из встроенных, но имеют семантику функций, а это означает, что перегруженный += оператор пользовательского типа, который появляется в выражении, фактически является вызовом функции. Поэтому не только введены точки последовательности, но и вся неопределенность, то выражение становится эквивалентным 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 не имеет ни undefined, ни неуказанного поведения в отношении порядка оценки. О точках последовательности не нужно беспокоиться. См. Спецификация языка Java 15.7. Порядок оценки:

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

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

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

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


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

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

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

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

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

Ответ 3

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

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

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

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

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.

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

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

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

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

ИЗМЕНИТЬ

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

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

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

Ответ 4

В соответствии со спецификацией языка 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);
    }
}

В порядке,

  • actual присваивается значение actualStart.X += actualStart.f().
  • expected присваивается значение
  • результат извлечения actualStart.X, который 0 и
  • применение оператора сложения к actualStart.X с помощью
  • возвращаемое значение вызова actualStart.f(), которое 1
  • и присвоить результат 0 + 1 expected.

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

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

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

Изменить:

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