Strange std :: comportement de la carte

Le programme de test suivant

#include  #include  using namespace std; int main(int argc, char **argv) { map a; a[1]=a.size(); for(map::const_iterator it=a.begin(); it!=a.end(); ++it) cout << "first " << (*it).first << " second " << (*it).second << endl; } 

conduit à une sortie différente quand compilé sur g++ 4.8.1 (Ubuntu 12.04 LTS):

 g++ xxx.cpp ./a.out first 1 second 1 

et sur Visual Studio 2012 (Windows 7) (Projet d’application console Win32 standard):

 ConsoleApplication1.exe first 1 second 0 

Quel compilateur a raison? Est-ce que je fais quelque chose de mal?

Il s’agit en fait d’un programme bien formé qui possède deux chemins d’exécution également valides, de sorte que les deux compilateurs ont raison.

 a[1] = a.size() 

Dans cette expression, l’évaluation des deux opérandes de = n’est pas séquencée.

§1.9 / 15 [intro.execution] Sauf indication contraire, les évaluations des opérandes des opérateurs individuels et des sous-expressions des expressions individuelles ne sont pas séquencées.

Cependant, les appels de fonctions ne sont pas entrelacés, de sorte que les appels à l’ operator[] et à la size sont en réalité indéfinis et non séquencés.

§1.9 / 15 [exécution.exécution] Toute évaluation de la fonction appelante (y compris les autres appels de fonction) qui n’est pas autrement séquencée de manière précise avant ou après l’exécution du corps de la fonction appelée est séquencée de manière indéterminée en ce qui concerne l’exécution de l’appelé. fonction.

Cela signifie que les appels de fonction peuvent se produire dans l’un des deux ordres suivants:

  1. operator[] puis size
  2. size alors operator[]

Si une clé n’existe pas et que vous appelez l’ operator[] avec cette clé, celle-ci sera ajoutée à la carte, modifiant ainsi la taille de la carte. Ainsi, dans le premier cas, la clé sera ajoutée, la taille sera récupérée (1 maintenant) et 1 sera affectée à cette clé. Dans le second cas, la taille sera récupérée (0), la clé sera ajoutée et 0 sera affecté à cette clé.

Notez que ce n’est pas une situation qui entraîne un comportement indéfini. Un comportement non défini se produit lorsque deux modifications ou une modification et une lecture du même object scalaire ne sont pas séquencées.

§1.9 / 15 [intro.execution] Si un effet latéral sur un object scalaire n’est pas séquencé par rapport à un autre effet latéral sur le même object scalaire ou à un calcul de valeur utilisant la valeur du même object scalaire, le comportement est indéfini.

Dans cette situation, ils ne sont pas séquencés mais indéfiniment séquencés.

Donc, ce que nous avons, ce sont deux commandes de l’exécution du programme également valables. Soit cela peut arriver et les deux donnent une sortie valide. C’est un comportement non spécifié .

§1.3.25 [defns.unspecified]
comportement non spécifié
comportement, pour une construction de programme bien formée et des données correctes, qui dépend de l’implémentation


Donc, pour répondre à vos questions:

Quel compilateur a raison?

Les deux sont.

Est-ce que je fais quelque chose de mal?

Probablement. Il est peu probable que vous souhaitiez écrire du code comportant deux chemins d’exécution comme celui-ci. Un comportement non spécifié peut être correct, contrairement à un comportement indéfini, car il peut être résolu en une seule sortie observable, mais cela ne vaut pas la peine d’être éliminé. Au lieu de cela, n’écrivez pas de code présentant ce type d’ambiguïté. Selon ce que vous voulez que le chemin correct soit, vous pouvez effectuer l’une des opérations suivantes:

 auto size = a.size(); a[1] = size; // value is 0 

Ou:

 a[1]; a[1] = a.size(); // value is 1 

Si vous voulez que le résultat soit 1 et que vous savez que la clé n’existe pas encore, vous pouvez bien sûr faire le premier code mais atsortingbuer la size + 1 .

Dans ce cas, où a[1] renvoie un type primitif, veuillez vous reporter à cette réponse . Dans le cas où le type de valeur std::map est un type défini par l’utilisateur et operator=(T, std::size_t) est défini pour ce type, l’expression:

 a[1] = a.size(); 

peut être converti en la version de sucre moins syntaxique correspondante:

 a[1] = a.size(); a.operator[](1) = a.size(); operator=(a.operator[](1), a.size()); 

Et comme nous le soaps tous au §8.3.6 / 9:

L’ordre d’évaluation des arguments de fonction n’est pas spécifié.

ce qui conduit au fait que le résultat de l’expression ci-dessus n’est pas spécifié .

Nous avons bien sûr deux cas:

  • Si le a.operator[](1) est évalué en premier, la taille de la carte est incrémentée de 1, ce qui conduit à la première sortie (les first 1 second 1 ).
  • Si a.size() est évalué en premier, le résultat obtenu est le second ( first 1 second 0 ).

Ceci est connu comme un problème de séquence qui signifie que certaines opérations peuvent être effectuées dans n’importe quel ordre choisi par le compilateur.

Si l’un a des effets secondaires, il est appelé “comportement non spécifié” un peu comme “comportement indéfini” mais où le résultat doit être l’un des sous-ensembles de résultats fixes, donc ici il doit être 0 ou 1 et peut ‘ t être une autre valeur. En réalité, vous devriez généralement éviter de le faire.

Dans votre cas particulier. l’ operator [] sur une carte change de taille (si cet élément n’existe pas encore). Ainsi, il a un effet secondaire sur le côté droit de ce qu’il lui atsortingbue.