De l’utilisation de &= et |=

Article original : About &= and |= | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

Il m’arrive assez souvent de voir, sur les projets assez vieux, l’utilisation des op√©rateurs &= et |= pour mettre √† jour un code d’erreur lors d’appels successifs de fonction.
Comme par exemple :

bool cr = true;
cr &= DoOneThing();
cr &= DoOneOtherThing();
cr &= DoALastThing();
if (cr)
    std::cout << "Success !" << std::endl;

Cette √©criture, bien concise et pratique, permet a priori d’√©viter l’√©criture plus verbeuse :

bool cr = true;
cr = cr && DoOneThing();
cr = cr && DoOneOtherThing();
cr = cr && DoALastThing();
if (cr)
    std::cout << "Success !" << std::endl;

Il faut faire attention cependant, car derri√®re l’emploi de &= et |= il y a un certain nombre de subtilit√©s qu’il est important que chacun comprenne.

D√©finition des op√©rateurs &= et |=

Commen√ßons par nous pencher sur ce que signifie r√©ellement les op√©rateurs &= et |=.
Dans cette section, j’utiliserai &= pour l’exemple, mais |= fonctionne de mani√®re strictement identique.

&= est un op√©rateur arithm√©tique binaire, qui r√©alise l’op√©ration et l’assignation du ET binaire.
Plus précisiémement, écrire ceci :

v1 &= v2;

est équivalent à écrire cela :

v1 = v1 & v2;

hormis que v1 ne sera √©valu√© qu’une seule fois.
Pour plus de pr√©cision, consultez le standard C++11 5.17 Assignment and compound assignment operators ou le lien suivant : https://en.cppreference.com/w/cpp/language/operator_arithmetic

Les op√©rations arithm√©tiques binaires (qui utilisent les symboles &|^<< et >>) effectuent des op√©rations bit-√†-bit sur chaque param√®tre de l’op√©ration.

Exemple :

int a = 0b110;
int b = 0b011;
int c = a & b; // 010
int d = a | b; // 111
int e = a ^ b; // 101

√Čcrire a &= b; fait donc un ET binaire entre a et b et assigne le r√©sultat √† a.

Différences entre &= et &&, |= et ||

Les op√©rateurs de comparaison && et || sont, eux, des op√©rateurs logiques. Ils sont donc diff√©rents des op√©rateur & et | qui sont des op√©rateurs arithm√©tiques.
L’utilisation des op√©rateur &= et |= dans un cadre logique peut donc diverger de ce √† quoi on peut s’attendre. Voici deux exemples illustrant des comportement inatendus.

Exemple 1

Prenons le code suivant :

#include <iostream>
 
bool foo()
{
    std::cout << "call foo()" << std::endl;
    return true;
}
 
int main() {
    bool c = true;
    std::cout << "Using |=" << std::endl;
    c |= foo();
    std::cout << "Using = and ||" << std::endl;
    c = c || foo();
    std::cout << "End" << std::endl;
    return 0;
}

La fonction foo() retourne toujours true, tout en √©crivant sur la sortie standard pour indiquer qu’elle a √©t√© appel√©e.

Dans le main() on cr√©e un bool√©en c, initialis√© √† true, et sur lequel on va successivement utiliser l’op√©rateur arithm√©tique binaire |= et l’op√©rateur logique ||.

Voici le résultat sur la sortie standard :

Using |=
call foo()
Using = and ||
End

On remarque que dans le cas de |= la fonction foo() est appel√©e, alors que ce n’est pas le cas dans le cas de ||.

Cela vient du fait qu’en C++, les op√©rateur logique sont lazy, c’est √† dire que si le param√®tre de droite ne change pas le r√©sultat de l’op√©ration, il ne sera pas √©valu√©.
Ici, comme c est d√©j√† true, alors que foo() soit true ou false ne changera pas le r√©sultat : il sera forc√©ment true.

Le cas de |= est une autre affaire. |= n’est pas un op√©rateur logique : il effectue une op√©ration arithm√©tique. Le r√©sultat qu’il donne ne d√©pend pas de l’√©valuation logique des param√®tres, mais de leur valeur r√©elle en binaire. Il n’y a pas de sens donc pour lui d’√™tre lazy. Dans le cas d’utilisation des op√©rateurs arithm√©tiques binaire, tous les param√®tres seront toujours √©valu√©s.

Que vous vouliez ou pas que le param√®tre de droite soit toujours √©valu√© d√©pend de vous et de votre contexte. Cependant, dans l’esprit de la plupart des d√©veloppeur, les op√©rations logiques sont lazy et ils auront tendance √† adopter ce point de vue par d√©faut.

Exemple 2

Voici un autre exemple qui, m√™me s’il est plus rare en pratique, peut cr√©er de r√©els probl√®mes difficiles √† investiguer.

#include <iostream>
 
int main() {
    int c1 = true;
    int c2 = 0b10;
 
    if (c1)
        std::cout << "c1 is true" <<std::endl;
    else
        std::cout << "c1 is false" <<std::endl;
 
    if (c2)
        std::cout << "c2 is true" <<std::endl;
    else
        std::cout << "c2 is false" <<std::endl;
 
    if (c1&&c2)
        std::cout << "c1&&c2 is true" <<std::endl;
    else
        std::cout << "c1&&c2 is false" <<std::endl;
 
    c1 &= c2;
 
    if (c1)
        std::cout << "c1&=c2 -> c1 is true" <<std::endl;
    else
        std::cout << "c1&=c2 -> c1 is false" <<std::endl;
 
    return 0;
}

Ici, on cr√©√© deux variable : c1, qui vaut true, et c2, qui vaut 0b010.

S’ensuivent quatre blocs effectuant des √©valuations logiques.
– On commence par √©valuer logiquement c1.
– On √©value de la m√™me mani√®re c2.
– On √©value l’op√©ration logique c1 && c2.
– On effectue l’op√©raion arithm√©tique binaire c1 &= c2 et on √©value c1.

Voici le résultat affiché sur la sortie standard :

c1 is true
c2 is true
c1&&c2 is true
c1&=c2 -> c1 is false

Détaillons en détail le pourquoi et le comment :

Premi√®rement, c1 est √©valu√© comme vrai. Ce qui est attendu, vu qu’on l’a initialis√© √† true.

Deuxi√®mement, c2 est √©valu√© comme vrai. En effet, c2 est un entier diff√©rent de 0, il est donc consid√©r√© comme logiquement vrai.

Troisi√®mement,¬†c1 && c2¬†est √©valu√© comme vrai. On vient de voir que¬†c1¬†et¬†c2¬†sont tous les deux vrais, et donc l’op√©ration logique¬†&&¬†renvoie vrai.

Quatri√®ment, on fait l’op√©ration c1 &= c2. En faisant cela, on effectue l’op√©ration c1 & c2 et on affecte le r√©sultat √† c1.
Hors, d’apr√®s le standard, true vaut 1 en tant qu’entier. c1 & c2 √©quivaut donc √† √©crire 0b01 & 0b10. Le r√©sultat de cette op√©ration est bien 0b00, car bit-√†-bit, l’op√©ration ET binaire est fausse sur tous les bits.
La valeur 0 √©tant √©valu√©e √† false lors d’une op√©ration logique, c1 est bien faux.

Ce n’est pas un cas qu’on rencontrera si souvent, mais il existe plein de cas pratiques, principalement dans du code legacy, o√Ļ on peut renvoyer une valeur autre que 1 en guise de valeur vraie.

Conclusion

L’emploi de &= et |= va faire ce qu’on attend de lui dans 99% des cas. Seulement c’est ce 1% qui va causer des bugs complexe √† d√©tecter et investiguer.

De plus, j’aimerais soulever un grave probl√®me de s√©mantique dans ces cas-l√†. Le fait d’utiliser une op√©ration arithm√©tique au lieu d’une op√©ration logique trompe le lecteur (ou le force √† faire un effort mental suppl√©mentaire) et emp√™che le compilateur de faire certaines optimisations (rappelez-vous que le compilateur est plus intelligent que nous et saura mieux optimiser, pour peu qu’on lui en laisse l’occasion).

√Čcrire a = a && b; plut√īt que a &= b; ne prends que quelques caract√®res suppl√©mentaires, mais saura vous pr√©server des bugs, rendra votre code plus int√®gre et permet au compilateur de comprendre ce que vous essayez de faire.

Voil√† pour cette semaine ! Si vous avez des remarques constructives √† faire, que vous soyez d’accord ou pas, n’h√©sitez pas √† le dire en commentaire.

NB : Le compilateur utilis√© pour ces exemples et clang, avec les options --std=c++20 -O3

Article original : About &= and |= | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

Laisser un commentaire