C++和PHP与C#和Java - 结果不一致

Dmi*_* K. 30 php c# c++ java expression-evaluation

我在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;
}
Run Code Online (Sandbox Code Playgroud)

在控制台中,您将看到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);
    }
}
Run Code Online (Sandbox Code Playgroud)

在一个控制台中你会看到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());
    }
}
Run Code Online (Sandbox Code Playgroud)

结果与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();
?>
Run Code Online (Sandbox Code Playgroud)

结果是11(看看结果在这里).

我有一点理论 - 这些语言(C#和Java)正在堆栈上制作静态变量X的本地副本(他们忽略了静态关键字吗?).这就是为什么这些语言的结果是1的原因.

有人在这,还有其他版本吗?

Chr*_*phe 48

C++标准规定:

对于不确定序列的函数调用,复合赋值的操作是单个评估.[注意:因此,函数调用不应介入左值到右值的转换和与任何单个复合赋值运算符相关的副作用. - 尾注]

§5.17[expr.ass]

因此,在您使用的相同评估X和具有副作用的函数中X,结果是未定义的,因为:

如果对标量对象的副作用相对于同一标量对象的另一个副作用或使用相同标量对象的值进行的值计算未被排序,则行为未定义.

§1.9[intro.execution]

它在许多编译器上恰好是11,但不能保证C++编译器不会像其他语言那样给你1.

如果你仍然持怀疑态度,对标准的另一个分析得出相同的结论:标准也在上面的同一部分中说:

表单的表达式的行为E1 op = E2等同于E1 = E1 op E2E1计算一次的表达式.

在你的情况下,X = X + f()除了X只评估一次.
由于评估顺序无法保证X + f(),您不能理所当然地认为第一个f被评估然后X.

附录

我不是Java专家,但Java规则明确指定了表达式中的评估顺序,在Java语言规范的第15.7节中保证从左到右.在第15.26.2.复合赋值运算符 Java规范也说E1 op= E2相当于E1 = (T) ((E1) op (E2)).

在您的Java程序中,这再次意味着您的表达式等效于,然后X = X + f()首先X进行求值f().因此,结果中f()不考虑副作用.

所以你的Java编译器没有bug.它符合规格.

  • 我假设所描绘的所有语言都包含一个类似的子句,并且所有这些语句的结果都是未定义的.他们恰好有所不同. (5认同)
  • @ Smith_61:没有.虽然Java基于Objective-C而不是C++,但是设计人员对C++很熟悉,并且由于他们使用C++的经验,他们非常刻意地决定不会有任何类型的JVM规范,Java语言规范或JRE规范中的未定义或实现定义的行为.*Iff*Java程序被*any*Java编译器接受,其结果将完全由规范定义.(浮点数周围有一些*平台定义的行为,当然还有非确定性的线程调度.) (5认同)
  • @ Smith_61这个假设可能会咬你.据我所知,它是为C#和Java定义的. (3认同)

Chr*_*ckl 21

感谢Deduplicator和user694733的评论,这是我原始答案的修改版本.


C++版本有 未定义未指明的行为.

"未定义"和"未指定"之间存在细微差别,前者允许程序执行任何操作(包括崩溃),而后者允许它从一组特定的允许行为中进行选择,而无需指示哪个选项是正确的.

除了非常罕见的情况,你总是希望避免这两种情况.


理解整个问题的一个很好的起点是C++ FAQs 为什么有些人认为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";
}
Run Code Online (Sandbox Code Playgroud)

这里的行为确实是未定义的,因为函数调用已经消失,因此x两个连续序列点之间都发生了两次修改.C++语言规范允许编译器创建一个程序,该程序打印123,崩溃或向您的老板发送一封侮辱性的电子邮件.

(当然,电子邮件的方式就是在解释如何一个很常见的幽默尝试不确定到底意味着任何事情都会发生.崩溃往往是不确定的行为更现实的结果.)

实际上,, 1(就像原始代码中的return语句一样)是一个红色的鲱鱼.以下结果也会产生未定义的行为:

#include <iostream>

int main() {
    int x = 0;
    x += (x += 10);
    std::cout << x << "\n";
}
Run Code Online (Sandbox Code Playgroud)

可能会打印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);
    }
}
Run Code Online (Sandbox Code Playgroud)

必须打印10.这是因为Java在评估顺序方面既没有未定义也没有未指定的行为.没有要关注的序列点.请参阅Java语言规范15.7.评估顺序:

Java编程语言保证运算符的操作数似乎以特定的评估顺序进行评估,即从左到右.

所以在Java情况下,x += (x += 10)从左到右解释,意味着第一个东西被添加到0,而某个东西是0 + 10.因此0 +(0 + 10)= 10.

另请参阅Java规范中的示例15.7.1-2.

回到原始示例,这也意味着静态变量的更复杂示例已在Java中定义和指定行为.


老实说,我不知道C#和PHP,但我猜他们都有一些保证评估顺序.与大多数其他编程语言(但与C一样)不同,C++倾向于允许比其他语言更多未定义和未指定的行为.这不是好事或坏事.这是稳健性和效率之间权衡.为特定任务或项目选择正确的编程语言始终是分析权衡的问题.

无论如何,具有这种副作用的表达式在所有四种语言中都是糟糕的编程风格.

最后一句话:

我在C#和Java中发现了一个小错误.

如果您没有多年的软件工程师专业经验,则不应该假设在语言规范编译器中发现错误.


Lua*_*aan 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

会产生11输出,而不是1.

这只是因为C#按顺序评估,所以在你的例子中,它首先读取x然后调用f(),而在我的情况下,它首先调用f()然后读取x.

现在,这仍然是不可能的.IL(.NET的字节码)+几乎与其他任何方法一样,但JIT编译器的优化可能会导致不同的评估顺序.另一方面,由于C#(和.NET)确实定义了评估/执行的顺序,所以我猜一个兼容的编译器应该总是产生这个结果.

在任何情况下,这是你发现的一个可爱的意外结果,并且一个警示故事 - 方法中的副作用即使在命令式语言中也是一个问题:)

哦,当然 - static意味着C#与C++有所不同.我已经看到C++ ers之前犯过的错误.

编辑:

让我稍微谈谈"不同语言"问题.您自动假设C++的结果是正确的,因为当您手动执行计算时,您正在按特定顺序进行评估 - 并且您已确定此顺序符合C++的结果.但是,C++和C#都没有对表达式进行分析 - 它只是对一些值的一堆操作.

C++ 确实存储x在寄存器中,就像C#一样.只是C#评估方法调用之前存储它,而C++在之后执行它.如果你更改C++代码x = f() + x,就像我在C#中所做的那样,我希望你能得到1输出.

最重要的是,C++(和C)根本没有指定操作的明确命令,可能是因为它想利用这一点做的那些订单的任何一个架构和平台.由于C#和Java的时候被开发时,这其实并不重要了,而且因为他们可以从C/C++的所有这些失败中吸取教训,他们指定的评估的明确命令.

  • -1."C++中参数的默认评估顺序是颠倒的".不,没有默认的评估顺序.我不确定允许参数评估在多大程度上重叠,但当然所有订单(包括但不限于从左到右和从右到左)都是允许的.而对于现代CPU的重新排序操作,C++甚至不保证一致性. (2认同)