30 votes

C ++ et PHP vs C # et Java - résultats inégaux

J'ai trouvé quelque chose d'un peu étrange, C# et Java. Regardons ce code 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;
}

Dans une console, vous verrez X = 11 (Regardez le résultat ici - IdeOne C++).

Examinons maintenant le même code 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);
    }
}

Dans une console, vous verrez 1 (pas 11!) (regardez le résultat ici - IdeOne C# Je sais ce que vous penser maintenant - "Comment est-ce possible?", mais nous allons les passer au code suivant.

Le code 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());
    }
}

Résultats de la même qu'en C# (X = 1, regardez le résultat ici).

Et pour la dernière fois, regardons le code 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();
?>

Le résultat est 11 (regardez ici).

J'ai un peu de théorie - ces langages (C# et Java) sont de faire une copie locale de la statique de la variable X sur la pile (sont-ils continuer à ignorer la statique mot-clé?). Et c'est la raison pourquoi ces deux langues est de 1.

Est quelqu'un, ici, qui ont d'autres versions?

48voto

Christophe Points 5220

La norme C++ états:

À l'égard d'un employé pour une période indéterminée-séquencé appel de la fonction, le fonctionnement d'un composé mission d'évaluation unique. [ Note: par conséquent, un appel de fonction ne doit pas intervenir entre la lvalue-à-rvalue de conversion et les effets secondaires associés à un seul opérateur d'assignation composé. -la note de fin ]

§5.17 [expr.cul]

Par conséquent, comme dans le même évaluation que vous utilisez X et une fonction avec un effet secondaire en X, le résultat est indéfini, parce que:

Si un effet indésirable sur un scalaire objet est séquencé par rapport à un autre effet secondaire sur le même scalaire objet ou d'une valeur de calcul à l'aide de la valeur de la même scalaire objet, le comportement est indéfini.

§1.9 [intro.exécution]

Il arrive à être 11 sur de nombreux compilateurs, mais il n'y a aucune garantie qu'un compilateur C++ ne vous donnera pas 1 comme pour les autres langues.

Si vous êtes encore sceptiques, une autre analyse de la norme conduit à la même conclusion: Le standard dit aussi dans la même section que ci-dessus:

Le comportement d'une expression de la forme E1 op = E2 est équivalent à E1 = E1 op E2 sauf qu' E1 est évaluée qu'une seule fois.

Dans votre cas X = X + f() sauf qu' X est évaluée qu'une seule fois.
Comme il n'y a pas de garantie sur l'ordre de l'évaluation, en X + f(), vous ne pouvez pas prendre pour acquis que le premier f est évaluée et puis, X.

Addendum: Je ne suis pas un expert java, mais la java des règles clairement spécifier l'ordre d'évaluation dans une expression, qui est garanti pour être de gauche à droite dans l'article 15.7 des spécifications java. Dans la section 15.26.2. Composé des Opérateurs d'Affectation java spécifications dire aussi que, E1 op= E2 est équivalent à E1 = (T) ((E1) op (E2)).

Dans votre java programm cela signifie encore que votre expression est équivalente à X = X + f() et le premier X est évalué, alors f(). Donc, l'effet secondaire de la f() n'est pas pris en compte dans le résultat.

Si votre compilateur java ne dispose pas d'un bug. Il juste est conforme aux spécifications.

21voto

Christian Hackl Points 4763

Edit: Merci pour les commentaires par Deduplicator et user694733, ici est une version modifiée de ma réponse originale à cette question.


La version C++ a pas défininon spécifiée comportement.

Il y a une subtile différence entre "undefined" et "non spécifié", en ce que la première permet à un programme de faire quelque chose (y compris de s'écraser) tandis que le second permet de choisir à partir d'un ensemble de notamment permis à des comportements sans dicter le choix est correct.

À l'exception de très rares cas, vous aurez toujours envie d'éviter à la fois.


Un bon point de départ pour comprendre l'ensemble de la question sont le C++ Faq Pourquoi certaines personnes pensent que x = ++o + o++ est mauvais? , Quelle est la valeur de i++ + ++ i? et Quel est le problème avec "séquence de points"?:

Entre le précédent et suivant de la séquence de point un scalaire objet est avoir son stockée la valeur modifiée au plus une fois par l'évaluation d'une de l'expression.

(...)

Pour l'essentiel, dans le C et le C++, si vous lisez une variable deux fois dans une expression où tu écris aussi, le résultat est indéfini.

(...)

À certains points spécifiques de la séquence d'exécution appelée séquence points, tous les effets secondaires des évaluations précédentes sont complètes et pas d'effets secondaires de la suite des évaluations ont eu lieu. (...) La "certains points" que l'on appelle séquence de points sont(...) après évaluation de l'ensemble de la fonction paramètres , mais avant la première expression au sein de la fonction est exécutée.

En bref, la modification d'une variable à deux reprises entre deux années consécutives de la séquence de points de rendements comportement indéfini, mais un appel de fonction introduit un intermédiaire point de séquence (en fait, deux intermédiaires séquence de points, parce que l'instruction de retour crée un autre).

Cela signifie que le fait que vous avez un appel de fonction dans votre expression "sauve" votre Simple::X += Simple::f(); ligne de est indéfini et il se transforme en "seulement" non spécifié.

1 et 11 sont possibles et de corriger les résultats, alors que l'impression 123, s'écraser ou de l'envoi de l'insulter e-mail à votre patron ne sont pas autorisés comportements; vous aurez jamais obtenir une garantie de savoir si 1 ou 11 sera imprimé.


L'exemple suivant est légèrement différente. C'est apparemment une simplification du code original mais vraiment sert à mettre en évidence la différence entre l'indéfini et indéterminé comportement:

#include <iostream>

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

Ici, le comportement est en effet pas défini, car l'appel de la fonction a disparu, de sorte que les deux modifications de l' x se produisent entre deux années consécutives de la séquence de points. Le compilateur est permis par le langage C++ cahier des charges pour créer un programme qui imprime 123, les accidents ou envoie une insulte e-mail à votre patron.

(L'e-mail de chose est bien sûr juste une très fréquent d'humour tenter d'expliquer comment indéfini signifie vraiment quelque chose se passe. Les pannes sont souvent un résultat plus réaliste du comportement indéfini.)

En fait, l' , 1 (tout comme l'instruction de retour dans votre code original) est un leurre. La suite de rendements comportement indéfini, trop:

#include <iostream>

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

Cela peut imprimer 20 (il le fait sur ma machine avec VC++ 2013), mais le comportement n'est toujours pas défini.

(Remarque: cela s'applique à des opérateurs. La surcharge d'opérateur modifie le comportement spécifié, parce que les opérateurs surchargés copie de la syntaxe à partir de l'intégré, mais ont la sémantique des fonctions, ce qui signifie que surchargé += opérateur de type personnalisé apparaît dans une expression est en fait un appel de fonction. Par conséquent, non seulement la séquence de points mis en place, mais l'ensemble de l'ambiguïté disparaît, l'expression de devenir l'équivalent d' x.operator+=(x.operator+=(10));, ce qui a garanti l'ordre de l'argument de l'évaluation. Ce n'est probablement pas pertinent à votre question, mais doit être mentionné de toute façon.)

En revanche, la version de Java

import java.io.*;

class Ideone
{
    public static void main(String[] args)
    {
        int x = 0;
        x += (x += 10);
        System.out.println(x);
    }
}

doit imprimer 10. C'est parce que Java n'a ni indéfini, ni quelconque comportement en ce qui concerne l'ordre d'évaluation. Il n'y a pas de séquence de points d'inquiétude. Voir Java Language Specification 15.7. Ordre D'Évaluation:

Le langage de programmation Java garantit que les opérandes de les opérateurs semblent être évalué dans un ordre d'évaluation, à savoir, de gauche à droite.

Ainsi, dans le Java cas, x += (x += 10), interprété à partir de la gauche vers la droite, signifie que la première chose est ajouté à 0, et que quelque chose est 0 + 10. Donc 0 + (0 + 10) = 10.

Voir aussi par exemple 15.7.1-2 dans la spécification de Java.

Pour en revenir à votre exemple d'origine, ce qui signifie aussi que l'exemple plus complexe avec la variable statique est définie et précisée comportement en Java.


Honnêtement, je ne sais pas à propos de C# et PHP mais je pense que les deux d'entre eux ont une garantie de l'évaluation et de commande ainsi. C++, contrairement à la plupart des autres langages de programmation (mais comme C) tend à permettre beaucoup plus d'indéfini et indéterminé de comportement que les autres langues. Ce n'est pas de bon ou de mauvais. C'est un compromis entre la robustesse et l'efficacité. Choisir le bon langage de programmation pour une tâche particulière ou d'un projet est toujours une question d'analyser les compromis.

Dans tous les cas, les expressions avec ces effets secondaires sont mauvais style de programmation dans les quatre langues.

Un dernier mot:

J'ai trouvé un petit bug dans le C# et Java.

Vous ne devriez pas supposer pour trouver des bogues dans les spécifications de langue ou de compilateurs si vous n'avez pas beaucoup d'années d'expérience professionnelle en tant qu'ingénieur logiciel.

7voto

Luaan Points 8934

Comme Christophe l'a déjà écrit, c'est, fondamentalement, un indéfini de l'opération.

Alors, pourquoi est-ce que C++ et PHP est-il un sens, C# et Java dans l'autre sens?

Dans ce cas (qui peut être différente pour différents compilateurs et les plates-formes), l'ordre d'évaluation des arguments en C++ est inversé par rapport à C# - C# évalue les arguments dans l'ordre de l'écriture, tandis que le C++ de l'échantillon est-il dans l'autre sens. Cela revient à la valeur par défaut des conventions d'appel à la fois utiliser, mais encore une fois - pour le C++, c'est un indéfini de l'opération, de sorte qu'il peut varier en fonction d'autres conditions.

Pour illustrer, ce code 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);
    }
}

Produira 11 sur la production, plutôt qu' 1.

C'est tout simplement parce que C# évalue "dans l'ordre", donc dans votre exemple, il lit d'abord x , puis appelle f(), tandis que dans le mien, il appelle d'abord f() puis de lectures x.

Maintenant, ce pourrait être unrealiable. IL (.NET du bytecode) a + comme à peu près toute autre méthode, mais des optimisations par le compilateur JIT pourrait résultat dans un ordre différent de l'évaluation. D'autre part, depuis C# (et .NET) n' définir l'ordre d'évaluation / d'exécution, donc je suppose que la conformité de compilateur doit toujours produire ce résultat.

En tout cas, c'est un joli résultat inattendu vous avez trouvé, et un conte d'avertissement - effets secondaires de méthodes peut être un problème, même dans des langages impératifs :)

Oh, et bien sûr - static signifie quelque chose de différent en C# et C++. J'ai vu que l'erreur commise par le C++ers à C# avant.

EDIT:

Permettez-moi seulement de développer un peu plus sur les "langues différentes" la question. Vous avez automatiquement supposer que C++résultat est correct, parce que quand vous faites le calcul à la main, vous êtes en train de faire l'évaluation dans un certain ordre et que vous avez déterminé ce afin de respecter les résultats de C++. Toutefois, ni le C++ et C# faire des analyses sur l'expression - c'est tout simplement un tas d'opérations sur certaines valeurs.

C++ ne stocker x dans un registre, exactement comme en C#. C'est juste que C# stocke avant l'évaluation de l'appel de méthode, tandis que le C++ est-il après. Si vous modifiez le code C++ pour faire x = f() + x au lieu de cela, comme je l'ai fait en C#, j'espère que vous aurez l' 1 sur la production.

La partie la plus importante est que le C++ (et C) n'a tout simplement pas de spécifier un ordre explicite d'opérations, probablement parce qu'il voulait exploiter les architectures et plates-formes qui ne soit un de ces ordres. Depuis C# et Java ont été développés à une époque où cela n'a pas vraiment d'importance, et de plus, étant donné qu'ils pourraient apprendre de toutes les défaillances de C/C++, ils ont spécifié un ordre explicite de l'évaluation.

4voto

jdphenix Points 2349

Selon le langage Java spécifications:

JLS 15.26.2, Composé des Opérateurs d'Affectation

Un composé de cession expression de la forme E1 op= E2 est équivalent à E1 = (T) ((E1) op (E2))T est le type de E1 sauf que E1 est évaluée qu'une seule fois.

Ce petit programme montre la différence, et des expositions comportement attendu basés sur ce standard.

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);
    }
}

Dans l'ordre,

  1. actual est affectée à la valeur de actualStart.X += actualStart.f().
  2. expected est affectée à la valeur de la
  3. résultat de la récupération de l' actualStart.X, ce qui est 0, et
  4. l'application de l'opérateur d'addition d' actualStart.X avec
  5. la valeur de retour de l'invocation d' actualStart.f(), ce qui est 1
  6. affectation du résultat de l' 0 + 1 de expected.

J'ai aussi déclaré diff , pour montrer comment la modification de l'ordre d'invocation change le résultat.

  1. diff est affectée à la valeur de la
  2. la valeur de retour de l'invocation d' diffStart.f(), avec est - 1, et
  3. l'application de l'opérateur d'addition à cette valeur avec
  4. la valeur de diffStart.X (qui est de 10, un effet secondaire de l' diffStart.f()
  5. affectation du résultat de l' 1 + 10 de diff.

En Java, c'est pas un comportement indéfini.

Edit:

Pour répondre à votre question au sujet des copies locales des variables. C'est exact, mais il n'a rien à voir avec static. Java enregistre le résultat de l'évaluation de chaque côté (côté gauche en premier), puis évalue la suite de l'exécution de l'opérateur sur les valeurs enregistrées.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X