30

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

4 に答える 4

48

C++ 標準には次のように記載されています。

不定順序の関数呼び出しに関しては、複合代入の操作は単一の評価です。[ 注: したがって、関数呼び出しは、左辺値から右辺値への変換と、単一の複合代入演算子に関連する副作用との間に介在してはなりません。—終わりのメモ]

§5.17 [expr.ass]

したがって、使用する同じ評価とXに副作用のある関数の場合と同様Xに、結果は未定義です。

スカラー オブジェクトに対する副作用が、同じスカラー オブジェクトに対する別の副作用、または同じスカラー オブジェクトの値を使用した値の計算に対して順序付けされていない場合、動作は未定義です。

§1.9 [イントロ.実行]

多くのコンパイラではたまたま 11 ですが、C++ コンパイラが他の言語のように 1 を返さないという保証はありません。

それでも懐疑的である場合は、標準の別の分析が同じ結論につながります。標準は、上記と同じセクションで次のようにも述べています。

フォームの式の動作は、1 回だけ評価されることを除いて、E1 op = E2と同じです。E1 = E1 op E2E1

あなたの場合、それは一度だけ評価されるX = X + f()ことを除いて。 評価の順序に保証がないため、 では、最初に f が評価され、次に が評価されることを当然のことと見なすことはできません。 X
X + 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 コンパイラにはバグがありません。仕様に準拠しているだけです。

于 2014-08-15T08:29:53.793 に答える
21

Deduplicator と user694733 によるコメントのおかげで、ここに私の元の回答の修正版があります。


C++ バージョンには未定義不特定の行動。

「未定義」と「未指定」には微妙な違いがあり、前者はプログラムが何でも(クラッシュを含む) 実行できるのに対し、後者は特定の許可された動作のセットから、どの選択が正しいかを指示することなく選択できます。

非常にまれなケースを除いて、常に両方を避けたいと思うでしょう。


問題全体を理解するための良い出発点は、C++ の FAQです。 x = ++y + y++ が悪いと考える人がいるのはなぜですか? i++ + i++ の値は何ですか? 「シーケンスポイント」との取引は何ですか?:

前のシーケンス ポイントと次のシーケンス ポイントの間で、スカラー オブジェクトの格納は、式の評価によって最大 1 回変更されます。

(...)

基本的に、C および C++ では、変数を記述している式で変数を 2 回読み取ると、結果はundefinedになります。

(...)

シーケンスポイントと呼ばれる実行シーケンスの特定のポイントでは、以前の評価のすべての副作用が完了し、後続の評価の副作用は発生していません。(...)シーケンス ポイントと呼ばれる「特定の指定ポイント」は、 (...) すべての関数のパラメーターの評価後、関数内の最初の式が実行される前です。

つまり、連続する 2 つのシーケンス ポイント間で変数を 2 回変更すると、未定義の動作が発生しますが、関数呼び出しによって中間シーケンス ポイントが導入されます (return ステートメントによって別のシーケンス ポイントが作成されるため、実際には 2 つの中間シーケンス ポイント)。

これは、式に関数呼び出しがあるという事実は、Simple::X += Simple::f();行が未定義になるのを「保存」し、それを「のみ」未指定に変えることを意味します。

1 と 11 の両方が可能で正しい結果ですが、123 を印刷したり、クラッシュしたり、上司に侮辱的な電子メールを送信したりすることは許可されていません。1 または 11 が出力されるかどうかは保証されません。


次の例は少し異なります。元のコードを簡略化したように見えますが、未定義の動作と未指定の動作の違いを強調するのに役立ちます。

#include <iostream>

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

ここでは、関数呼び出しがなくなったため、動作は実際には未定義です。したがって、両方の変更がx2 つの連続するシーケンス ポイント間で発生します。コンパイラは、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 言語仕様 15.7を参照してください。評価順序:

Java プログラミング言語は、演算子のオペランドが特定の評価順序 (左から右) で評価されるように見えることを保証します。

したがって、Java の場合、x += (x += 10)は左から右に解釈され、最初に何かが0に追加され、何かが0 + 10であることを意味します。したがって、 0 + (0 + 10) = 10です。

Java 仕様の例 ​​15.7.1-2 も参照してください。

元の例に戻ると、これは、静的変数を使用したより複雑な例が Java で動作を定義および指定したことも意味します。


正直なところ、C# と PHP についてはわかりませんが、どちらも評価順序が保証されていると思います。C++ は、他のほとんどのプログラミング言語とは異なり (ただし C と同様)、他の言語よりもはるかに多くの未定義および未指定の動作を許可する傾向があります。それは良くも悪くもありません。これは、堅牢性と効率のトレードオフです。特定のタスクまたはプロジェクトに適したプログラミング言語を選択するには、常にトレードオフを分析する必要があります。

いずれにせよ、このような副作用を伴う式は、4 つの言語すべてでプログラミング スタイルが悪いものです

最後に一言:

C# と Java で小さなバグを見つけました。

ソフトウェア エンジニアとしての長年の専門的経験がない場合は、言語仕様コンパイラにバグが見つかると思い込んではいけません。

于 2014-08-15T09:23:44.347 に答える
7

クリストフがすでに書いているように、これは基本的に未定義の操作です。

では、なぜ 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++er が犯した間違いを見たことがあります。

編集

「異なる言語」の問題について少し詳しく説明します。C++ の結果が正しいものであると自動的に仮定しました。これは、手動で計算を行っている場合、特定の順序で評価を行っているためです。この順序は、C++ の結果に準拠するように決定されています。ただし、C++ も C# も式の分析を行いません。これは単にいくつかの値に対する一連の操作です。

C++x、C# と同様にレジスタに格納します。C#はメソッド呼び出しを評価する前にそれを格納するのに対し、C++ は のに格納します。代わりに C++ コードを do に変更すると、C# で行ったのと同じように、 on 出力x = f() + xが得られると思います。1

最も重要な部分は、C++ (および C) が操作の明示的な順序を指定しなかったことです。おそらく、これらの順序のいずれかを実行するアーキテクチャとプラットフォームを利用したかったためです。C# と Java が開発されたのは、これがもはや問題にならない時代であり、C/C++ のすべての失敗から学ぶことができるため、評価の明示的な順序を指定しました。

于 2014-08-15T08:43:21.243 に答える
4

Java 言語仕様によると、次のようになります。

JLS 15.26.2、複合代入演算子

の形式の複合代入式 E1 op= E2 は と等価ですが E1 = (T) ((E1) op (E2))T は の型ですが E1E1 は​​ 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);
    }
}

順番に、

  1. actualの値に割り当てられますactualStart.X += actualStart.f()
  2. expectedの値に割り当てられます。
  3. を取得した結果actualStart.X、これは0であり、
  4. actualStart.Xwithに加算演算子を適用する
  5. invoking の戻り値actualStart.f()、つまり1
  6. の結果を に代入0 + 1expectedます。

diffまた、呼び出しの順序を変更すると結果がどのように変化するかを示すことも宣言しました。

  1. diffの値に割り当てられます
  2. の呼び出しの戻り値diffStart.f()with is 1、および
  3. でその値に加算演算子を適用する
  4. の値diffStart.X(これは 10 であり、diffStart.f()
  5. の結果を に代入1 + 10diffます。

Java では、これは未定義の動作ではありません。

編集:

変数のローカルコピーに関するポイントに対処するには。それは正しいが、それとは何の関係もないstatic. Java は、各辺 (最初に左側) を評価した結果を保存し、保存された値に対して演算子を実行した結果を評価します。

于 2014-08-15T08:51:43.587 に答える