我在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(Look at the result here - 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,查看结果here)。
最后一次让我们来看看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(查看结果here)。
我有一点理论 - 这些语言(C#和Java)正在堆栈上制作静态变量X的本地副本(他们是否忽略静态关键字?)。这就是为什么这些语言的结果是1的原因。
有人在这,还有其他版本吗?
答案 0 :(得分:48)
C ++标准规定:
对于不确定顺序的函数调用,复合赋值的操作是单个评估。 [注意:因此,函数调用不应介入左值到右值的转换和与任何单个复合赋值运算符相关的副作用。 - 后注]
§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规则明确指定了表达式中的评估顺序,在Java Language Specifications的第15.7节中保证从左到右。在 15.26.2节中。复合赋值运算符 Java规范还说E1 op= E2
等同于E1 = (T) ((E1) op (E2))
。
在您的Java程序中,这再次意味着您的表达式等同于X = X + f()
,并且首先评估X
,然后f()
。因此,结果中不考虑f()
的副作用。
所以你的Java编译器没有bug。它符合规格。
答案 1 :(得分:21)
感谢Deduplicator和user694733的评论,这是我原始答案的修改版本。
C ++版本有 undefined 未指定的行为。
&#34; undefined&#34;之间有一个微妙的区别。并且&#34;未指定&#34;,前者允许程序执行任何(包括崩溃),而后者允许它从一组特定的允许行为中进行选择,而无需指定哪个选项是正确的。
除了非常罕见的情况,你总是希望避免这两种情况。
理解整个问题的一个很好的起点是C ++常见问题Why do some people think x = ++y + y++ is bad? ,What’s the value of i++ + i++?和What’s the deal with “sequence points”?:
在前一个和下一个序列点之间标量对象应该 通过评估a,将其存储的值最多修改一次 表达
(...)
基本上,在C和C ++中,如果在表达式中读取变量两次 在你写它的地方,结果是未定义。
(...)
在执行序列中的某些指定点,称为序列 要点,先前评估的所有副作用应完整 不得进行后续评估的副作用。 (......) 被称为序列点的“某些指定点”是(...) 评估所有函数的参数之后但在第一次之前 执行函数中的表达式。
简而言之,在两个连续序列点之间修改变量两次会产生未定义的行为,但函数调用会引入一个中间序列点(实际上,两个中间序列点,因为return语句会创建另一个序列点)。
这意味着您在表达式中有一个函数调用&#34; save&#34;您的Simple::X += Simple::f();
行未定义并将其转换为&#34;仅#34;未指定的。
1和11都是可能和正确的结果,而打印123,崩溃或向您的老板发送侮辱性电子邮件是不允许的行为;你永远不会得到保证是打印1还是11。
以下示例略有不同。它似乎是对原始代码的简化,但确实有助于强调未定义和未指定行为之间的区别:
#include <iostream>
int main() {
int x = 0;
x += (x += 10, 1);
std::cout << x << "\n";
}
这里的行为确实是未定义的,因为函数调用已经消失,因此x
的两个修改都发生在两个连续的序列点之间。 C ++语言规范允许编译器创建一个打印123,崩溃或向您的老板发送侮辱性电子邮件的程序。
(电子邮件当然只是一种非常普遍的幽默尝试,用于解释 undefined 的真正含义任何事情。崩溃通常是未定义的更真实的结果行为)。
事实上,, 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 Language Specification 15.7. Evaluation Order:
Java编程语言保证了操作数 运算符似乎在特定的评估顺序中进行评估, 即,从左到右。
因此,在Java情况下,从左到右解释x += (x += 10)
意味着首先将某些内容添加到 0 ,并且某些内容 0 + 10 。因此 0 +(0 + 10)= 10 。
另请参阅Java规范中的示例15.7.1-2。
回到原始示例,这也意味着静态变量的更复杂示例已在Java中定义和指定行为。
老实说,我不了解C#和PHP,但我猜他们两个都有一些保证评估顺序。与大多数其他编程语言(但与C一样)不同,C ++倾向于允许比其他语言更多未定义和未指定的行为。这不是好事或坏事。它是健壮性和效率之间的权衡。为特定任务或项目选择正确的编程语言总是需要分析权衡。
在任何情况下,具有此类副作用的表达都是所有四种语言中的错误编程风格。
最后一句话:
我在C#和Java中发现了一个小错误。
如果您没有多年的软件工程师专业经验,则不应该假设在语言规范或编译器中发现错误。
答案 2 :(得分:7)
Christophe已经写过,这基本上是一个未定义的操作。
那么为什么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 ++的所有失败中学习,因此他们指定了明确的评估顺序。
答案 3 :(得分:4)
根据Java语言规范:
JLS 15.26.2, Compound Assignment Operators
表单的复合赋值表达式
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保存评估每一侧的结果(左侧第一个),然后评估对保存的值执行运算符的结果。