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 в стеке (игнорируют ли они ключевое слово static ?). И именно по этой причине результат на этих языках равен 1.

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

Author: Peter O., 2014-08-15

4 answers

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

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

§ 5.17 [экспр.жопа]

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

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

§ 1.9 [вступление к исполнению]

Во многих компиляторах это число равно 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
Author: Christophe, 2018-02-23 20:48:27

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


Версия 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 (точно так же, как оператор return в вашем исходном коде) является отвлекающим маневром. Следующее также приводит к неопределенному поведению:

#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.

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

 21
Author: Christian Hackl, 2017-12-05 20:59:13

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

Так почему же 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
Author: Luaan, 2014-08-15 11:30:17

В соответствии со спецификацией языка 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
Author: jdphenix, 2014-08-15 09:03:39