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.
Есть здесь кто-нибудь, у кого есть другие версии?
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 нет ошибки. Он просто соответствует техническим требованиям.
Благодаря комментариям дедупликатора и пользователя 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.
Вы не должны предполагать, что найдете ошибки в языковых спецификациях или компиляторах, если у вас нет многолетнего профессионального опыта в качестве инженера-программиста.
Как уже писал Кристоф, это в основном неопределенная операция.
Так почему же 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++, они указали явный порядок оценки.
В соответствии со спецификацией языка 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 это не неопределенное поведение.
Изменить:
Чтобы ответить на вашу точку зрения относительно локальных копий переменных. Это верно, но это не имеет никакого отношения к static
. Java сохраняет результат оценки каждой стороны (сначала с левой стороны), затем оценивает результат выполнения оператора по сохраненным значениям.