web-dev-qa-db-fra.com

Pourquoi a = (a + b) - (b = a) un mauvais choix pour échanger deux entiers?

Je suis tombé sur ce code pour échanger deux entiers sans utiliser de variable temporaire ni utiliser d'opérateurs au niveau du bit.

int main(){

    int a=2,b=3;
    printf("a=%d,b=%d",a,b);
    a=(a+b)-(b=a);
    printf("\na=%d,b=%d",a,b);
    return 0;
}

Mais je pense que ce code a un comportement indéfini dans l'instruction swap a = (a+b) - (b=a); car il ne contient aucun points de séquence pour déterminer l'ordre d'évaluation.

Ma question est: Est-ce une solution acceptable pour échanger deux entiers?

58
ashfaque

Non, ce n'est pas acceptable. Ce code appelle ( Comportement indéfini . Cela est dû au fait que l'opération sur b n'est pas définie. Dans l'expression

a=(a+b)-(b=a);  

il n'est pas certain que b soit modifié en premier ou que sa valeur soit utilisée dans l'expression (a+b) en raison de l'absence de point de séquence .
Voir ce syas standard:

C11: 6.5 Expressions:

Si un effet secondaire sur un objet scalaire n'est pas séquencé par rapport à un effet secondaire différent sur le même objet scalaire ou un calcul de valeur utilisant la valeur du même objet scalaire , le comportement n'est pas défini. S'il existe plusieurs ordres autorisés des sous-expressions d'une expression, le comportement n'est pas défini si un tel effet secondaire non séquencé se produit dans l'un des ordres.84) 1.

Lisez C-faq- 3.8 et ceci réponse pour une explication plus détaillée du point de séquence et du comportement indéfini.


1. L'accent est le mien.

83
haccks

Ma question est - est-ce une solution acceptable pour échanger deux entiers?

Acceptable pour qui? Si vous demandez si cela est acceptable pour moi, cela ne permettrait pas de passer outre toute révision de code dans laquelle j'étais, croyez-moi.

pourquoi a = (a + b) - (b = a) un mauvais choix pour échanger deux entiers?

Pour les raisons suivantes:

1) Comme vous le constatez, rien ne garantit en C qu'il le fait réellement. Ça pourrait faire n'importe quoi.

2) Supposons pour les besoins de l'argument qu'il permute vraiment deux entiers, comme il le fait en C #. (C # garantit que les effets secondaires se produisent de gauche à droite.) Le code serait toujours inacceptable car il n'est pas du tout évident de sa signification! Le code ne devrait pas être un tas d'astuces intelligentes. Écrivez le code de la personne qui vient après vous et qui doit le lire et le comprendre.

3) Encore une fois, supposons que cela fonctionne. Le code est toujours inacceptable car c'est tout simplement faux:

Je suis tombé sur ce code pour échanger deux entiers sans utiliser de variable temporaire ni utiliser d'opérateurs bit à bit.

C'est tout simplement faux. Cette astuce utilise une variable temporaire pour stocker le calcul de a+b. La variable est générée par le compilateur en votre nom et ne reçoit pas de nom, mais elle est là. Si l'objectif est d'éliminer les temporaires, cela ne fait qu'empirer, pas mieux! Et pourquoi voudriez-vous éliminer les temporaires en premier lieu? Ils sont bon marché!

4) Cela ne fonctionne que pour les entiers. Beaucoup de choses doivent être échangées autres que des entiers.

En bref, passez votre temps à vous concentrer sur l'écriture de code qui est évidemment correct, plutôt que d'essayer de trouver des astuces intelligentes qui aggravent les choses.

48
Eric Lippert

Il y a au moins deux problèmes avec a=(a+b)-(b=a).

Celui que vous mentionnez vous-même: le manque de points de séquence signifie que le comportement n'est pas défini. En tant que tel, tout pouvait arriver. Par exemple, aucune garantie n'est évaluée en premier: a+b ou b=a. Le compilateur peut choisir de générer d'abord du code pour l'affectation, ou faire quelque chose de complètement différent.

Un autre problème est le fait que le dépassement de l'arithmétique signée est un comportement indéfini. Si a+b débordements il n'y a aucune garantie des résultats; même une exception pourrait être levée.

32
Joni

Mis à part les autres réponses, sur le comportement et le style indéfinis, si vous écrivez du code simple qui utilise simplement une variable temporaire, le compilateur peut probablement tracer les valeurs et ne pas les échanger réellement dans le code généré, et utiliser simplement les valeurs échangées plus tard dans certains cas. Il ne peut pas le faire avec votre code. Le compilateur est généralement meilleur que vous lors des micro-optimisations.

Il est donc probable que votre code soit plus lent, plus difficile à comprendre et probablement un comportement non défini non fiable.

29
jcoder

Si vous utilisez gcc et -Wall le compilateur vous prévient déjà

a.c: 3: 26: avertissement: l'opération sur ‘b’ peut être indéfinie [-Wsequence-point]

L'utilisation ou non d'une telle construction est également discutable du point de vue des performances. Quand on regarde

void swap1(int *a, int *b)
{
    *a = (*a + *b) - (*b = *a);
}

void swap2(int *a, int *b)
{
    int t = *a;
    *a = *b;
    *b = t;
}

et examiner le code d'assemblage

swap1:
.LFB0:
    .cfi_startproc
    movl    (%rdi), %edx
    movl    (%rsi), %eax
    movl    %edx, (%rsi)
    movl    %eax, (%rdi)
    ret
    .cfi_endproc

swap2:
.LFB1:
    .cfi_startproc
    movl    (%rdi), %eax
    movl    (%rsi), %edx
    movl    %edx, (%rdi)
    movl    %eax, (%rsi)
    ret
    .cfi_endproc

vous ne voyez aucun avantage à brouiller le code.


En regardant le code C++ (g ++), qui fait essentiellement la même chose, mais prend en compte move

#include <algorithm>

void swap3(int *a, int *b)
{
    std::swap(*a, *b);
}

donne une sortie d'assemblage identique

_Z5swap3PiS_:
.LFB417:
    .cfi_startproc
    movl    (%rdi), %eax
    movl    (%rsi), %edx
    movl    %edx, (%rdi)
    movl    %eax, (%rsi)
    ret
    .cfi_endproc

Tenant compte de l'avertissement de gcc et ne voyant aucun gain technique, je dirais que je m'en tiens aux techniques standard. Si cela devient un goulot d'étranglement, vous pouvez toujours rechercher comment améliorer ou éviter ce petit morceau de code.

28
Olaf Dietsche

La déclaration:

a=(a+b)-(b=a);

invoque un comportement indéfini. La seconde doit être violée dans le paragraphe cité:

(C99, 6.5p2) "Entre le point de séquence précédent et le point de séquence suivant, un objet doit voir sa valeur stockée modifiée au plus une fois par l'évaluation d'une expression. En outre, la valeur antérieure doit être lue uniquement pour déterminer la valeur à être stocké. "

8
ouah

Une question a été publiée dans 201 avec exactement le même exemple.

a = (a+b) - (b=a);

Steve Jessop met en garde contre cela:

Soit dit en passant, le comportement de ce code n'est pas défini. A et b sont lus et écrits sans point de séquence intermédiaire. Pour commencer, le compilateur aurait tout à fait le droit d'évaluer b = a avant d'évaluer a + b.

Voici une explication d'une question publiée dans 2012 . Notez que l'échantillon n'est pas exactement le même en raison du manque de parenthèses, mais la réponse reste néanmoins pertinente.

En C++, les sous-expressions dans les expressions arithmétiques n'ont pas d'ordre temporel.

a = x + y;

Est-ce que x est évalué en premier, ou y? Le compilateur peut choisir soit, soit il peut choisir quelque chose de complètement différent. L'ordre d'évaluation n'est pas la même chose que la priorité des opérateurs: la priorité des opérateurs est strictement définie, et l'ordre d'évaluation est uniquement défini en fonction de la granularité que votre programme a des points de séquence.

En fait, sur certaines architectures, il est possible d'émettre du code qui évalue à la fois x et y - par exemple, les architectures VLIW.

Maintenant pour les citations standard C11 de N1570:

Annexe J.1/1

Il s'agit d'un comportement non spécifié lorsque:

- L'ordre dans lequel les sous-expressions sont évaluées et l'ordre dans lequel les effets secondaires se produisent, sauf comme spécifié pour l'appel de fonction (), &&, ||, ? : et opérateurs virgule (6.5).

- L'ordre dans lequel les opérandes d'un opérateur d'affectation sont évalués (6.5.16).

Annexe J.2/1

Il s'agit d'un comportement non défini lorsque:

- Un effet secondaire sur un objet scalaire n'est pas séquencé par rapport à un effet secondaire différent sur le même objet scalaire ou à un calcul de valeur utilisant la valeur du même objet scalaire (6.5).

6,5/1

Une expression est une séquence d'opérateurs et d'opérandes qui spécifie le calcul d'une valeur, ou qui désigne un objet ou une fonction, ou qui génère des effets secondaires, ou qui effectue une combinaison de ceux-ci. Les calculs de valeur des opérandes d'un opérateur sont séquencés avant le calcul de valeur du résultat de l'opérateur.

6,5/2

Si un effet secondaire sur un objet scalaire n'est pas séquencé par rapport à un effet secondaire différent sur le même objet scalaire ou à un calcul de valeur utilisant la valeur du même objet scalaire, le comportement n'est pas défini. S'il existe plusieurs ordres autorisés des sous-expressions d'une expression, le comportement n'est pas défini si un tel effet secondaire non séquencé se produit dans l'un des ordres.84)

6.5/3

Le regroupement des opérateurs et des opérandes est indiqué par la syntaxe.85) Sauf spécification ultérieure, les effets secondaires et les calculs de valeur des sous-expressions ne sont pas séquencés.86)

Vous ne devez pas vous fier à un comportement non défini.

Quelques alternatives: En C++, vous pouvez utiliser

  std::swap(a, b);

Échange XOR:

  a = a^b;
  b = a^b;
  a = a^b;
8
user1508519

Le problème est que selon la norme C++

Sauf indication contraire, les évaluations d'opérandes d'opérateurs individuels et de sous-expressions d'expressions individuelles ne sont pas séquencées.

Donc, cette expression

a=(a+b)-(b=a);

a un comportement indéfini.

5
Vlad from Moscow

Vous pouvez utiliser algorithme de swap XOR pour éviter tout problème de débordement et avoir toujours une ligne.

Mais comme vous avez une balise c++, Je préfère simplement une simple std::swap(a, b) pour la rendre plus lisible.

5
dreamzor

Outre les problèmes invoqués par les autres utilisateurs, ce type de swap a un autre problème: le domaine.

Et qu'est-ce qui se passerait si a+b va au-delà de sa limite? Disons que nous travaillons avec un nombre de 0 à 100. 80+ 60 serait hors de portée.

4
catalinux