Comment éviter une chaîne if / else if lors de la classification d'un cap en 8 directions?


111

J'ai le code suivant:

if (this->_car.getAbsoluteAngle() <= 30 || this->_car.getAbsoluteAngle() >= 330)
  this->_car.edir = Car::EDirection::RIGHT;
else if (this->_car.getAbsoluteAngle() > 30 && this->_car.getAbsoluteAngle() <= 60)
  this->_car.edir = Car::EDirection::UP_RIGHT;
else if (this->_car.getAbsoluteAngle() > 60 && this->_car.getAbsoluteAngle() <= 120)
  this->_car.edir = Car::EDirection::UP;
else if (this->_car.getAbsoluteAngle() > 120 && this->_car.getAbsoluteAngle() <= 150)
  this->_car.edir = Car::EDirection::UP_LEFT;
else if (this->_car.getAbsoluteAngle() > 150 && this->_car.getAbsoluteAngle() <= 210)
  this->_car.edir = Car::EDirection::LEFT;
else if (this->_car.getAbsoluteAngle() > 210 && this->_car.getAbsoluteAngle() <= 240)
  this->_car.edir = Car::EDirection::DOWN_LEFT;
else if (this->_car.getAbsoluteAngle() > 240 && this->_car.getAbsoluteAngle() <= 300)
  this->_car.edir = Car::EDirection::DOWN;
else if (this->_car.getAbsoluteAngle() > 300 && this->_car.getAbsoluteAngle() <= 330)
  this->_car.edir = Car::EDirection::DOWN_RIGHT;

Je veux éviter la ifchaîne s; c'est vraiment moche. Y a-t-il une autre façon, peut-être plus propre, d'écrire cela?


77
@Oraekia Cela aurait l'air beaucoup moins laid, moins à taper et mieux à lire si vous fectchiez la this->_car.getAbsoluteAngle()fois avant toute la cascade.
πάντα ῥεῖ

26
Tout ce déréférencement explicite de this( this->) n'est pas nécessaire et ne fait rien de bon pour la lisibilité.
Jesper Juhl

2
@Neil Paire en tant que clé, enum en tant que valeur, recherche personnalisée lambda.
πάντα ῥεῖ

56
Le code serait beaucoup moins laid sans tous ces >tests; ils ne sont pas nécessaires, car chacun d'eux a déjà été testé (dans la direction opposée) dans l' ifinstruction précédente .
Pete Becker

10
@PeteBecker C'est l'un de mes chouchous sur un code comme celui-ci. Trop de programmeurs ne comprennent pas else if.
Barmar

Réponses:


176
#include <iostream>

enum Direction { UP, UP_RIGHT, RIGHT, DOWN_RIGHT, DOWN, DOWN_LEFT, LEFT, UP_LEFT };

Direction GetDirectionForAngle(int angle)
{
    const Direction slices[] = { RIGHT, UP_RIGHT, UP, UP, UP_LEFT, LEFT, LEFT, DOWN_LEFT, DOWN, DOWN, DOWN_RIGHT, RIGHT };
    return slices[(((angle % 360) + 360) % 360) / 30];
}

int main()
{
    // This is just a test case that covers all the possible directions
    for (int i = 15; i < 360; i += 30)
        std::cout << GetDirectionForAngle(i) << ' ';

    return 0;
}

Voilà comment je le ferais. (Selon mon commentaire précédent).


92
J'ai littéralement cru voir le code Konami à la place de l'énumération pendant une seconde.
Zano

21
@CodesInChaos: C99 et C ++ ont la même exigence que C #: que si q = a/bet r = a%balors q * b + rdoivent être égaux a. Il est donc légal en C99 qu'un reste soit négatif. BorgLeader, vous pouvez résoudre le problème avec (((angle % 360) + 360) % 360) / 30.
Eric Lippert

7
@ericlippert, vous et vos connaissances en mathématiques computationnelles continuent d'impressionner.
gregsdennis

33
C'est très intelligent, mais c'est complètement illisible, et il n'est plus probable que ce soit maintenable, donc je ne suis pas d'accord pour dire que c'est une bonne solution à la "laideur" perçue de l'original. Il y a un élément de goût personnel, ici, je suppose, mais je trouve les versions de branchement nettoyées par x4u et motoDrizzt largement préférables.
IMSoP

4
@cyanbeam la boucle for dans main est juste une "démo", GetDirectionForAnglec'est ce que je propose en remplacement de la cascade if / else, ils sont tous les deux O (1) ...
Borgleader

71

Vous pouvez utiliser map::lower_bound et stocker la limite supérieure de chaque angle dans une carte.

Exemple de travail ci-dessous:

#include <cassert>
#include <map>

enum Direction
{
    RIGHT,
    UP_RIGHT,
    UP,
    UP_LEFT,
    LEFT,
    DOWN_LEFT,
    DOWN,
    DOWN_RIGHT
};

using AngleDirMap = std::map<int, Direction>;

AngleDirMap map = {
    { 30, RIGHT },
    { 60, UP_RIGHT },
    { 120, UP },
    { 150, UP_LEFT },
    { 210, LEFT },
    { 240, DOWN_LEFT },
    { 300, DOWN },
    { 330, DOWN_RIGHT },
    { 360, RIGHT }
};

Direction direction(int angle)
{
    assert(angle >= 0 && angle <= 360);

    auto it = map.lower_bound(angle);
    return it->second;
}

int main()
{
    Direction d;

    d = direction(45);
    assert(d == UP_RIGHT);

    d = direction(30);
    assert(d == RIGHT);

    d = direction(360);
    assert(d == RIGHT);

    return 0;
}

Aucune division requise. Bien!
O. Jones

17
@ O.Jones: La division par une constante de compilation est assez bon marché, juste une multiplication et quelques décalages. J'irais avec l'une des table[angle%360/30]réponses parce qu'elle est bon marché et sans succursale. Beaucoup moins cher qu'une boucle de recherche arborescente, si cela compile en asm qui est similaire à la source. ( std::unordered_mapest généralement une table de hachage, mais std::mapest généralement un arbre binaire rouge-noir. La réponse acceptée est angle%360 / 30utilisée comme une fonction de hachage parfaite pour les angles (après avoir répliqué quelques entrées, et la réponse de Bijay évite même cela avec un décalage)).
Peter Cordes

2
Vous pouvez utiliser lower_boundsur un tableau trié. Ce serait beaucoup plus efficace qu'un map.
wilx

La recherche de carte @PeterCordes est facile à écrire et à maintenir. Si les plages changent, la mise à jour du code de hachage peut introduire des bogues et si les plages deviennent non uniformes, elles peuvent simplement s'effondrer. À moins que ce code ne soit critique pour les performances, je ne dérangerais pas.
OhJeez

@OhJeez: Ils ne sont déjà pas uniformes, ce qui est géré en ayant la même valeur dans plusieurs buckets. Utilisez simplement un diviseur plus petit pour obtenir plus de compartiments, à moins que cela signifie utiliser un très petit diviseur et avoir beaucoup trop de compartiments. De plus, si les performances n'ont pas d'importance, alors une chaîne if / else n'est pas mauvaise non plus, si elle est simplifiée en factorisant this->_car.getAbsoluteAngle()avec une variable tmp, et en supprimant la comparaison redondante de chacune des if()clauses de l'OP (vérifier quelque chose qui aurait déjà correspondu le précédent if ()). Ou utilisez la suggestion de tableau trié de @ wilx.
Peter Cordes

58

Créez un tableau dont chaque élément est associé à un bloc de 30 degrés:

Car::EDirection dirlist[] = { 
    Car::EDirection::RIGHT, 
    Car::EDirection::UP_RIGHT, 
    Car::EDirection::UP, 
    Car::EDirection::UP, 
    Car::EDirection::UP_LEFT, 
    Car::EDirection::LEFT, 
    Car::EDirection::LEFT, 
    Car::EDirection::DOWN_LEFT,
    Car::EDirection::DOWN, 
    Car::EDirection::DOWN, 
    Car::EDirection::DOWN_RIGHT, 
    Car::EDirection::RIGHT
};

Ensuite, vous pouvez indexer le tableau avec l'angle / 30:

this->_car.edir = dirlist[(this->_car.getAbsoluteAngle() % 360) / 30];

Aucune comparaison ou branchement requis.

Le résultat est cependant légèrement différent de l'original. Les valeurs sur les bordures, c'est-à-dire 30, 60, 120, etc. sont placées dans la catégorie suivante. Par exemple, dans le code d'origine, les valeurs valides pour UP_RIGHTsont de 31 à 60. Le code ci-dessus affecte 30 à 59 à UP_RIGHT.

Nous pouvons contourner cela en soustrayant 1 de l'angle:

this->_car.edir = dirlist[((this->_car.getAbsoluteAngle() - 1) % 360) / 30];

Cela nous donne maintenant RIGHTpour 30, UP_RIGHTpour 60, etc.

Dans le cas de 0, l'expression devient (-1 % 360) / 30. Ceci est valable parce que -1 % 360 == -1et -1 / 30 == 0, donc nous obtenons toujours un index de 0.

La section 5.6 de la norme C ++ confirme ce comportement:

4 L' /opérateur binaire donne le quotient, et l' %opérateur binaire donne le reste de la division de la première expression par la seconde. Si le deuxième opérande de /ou %est égal à zéro, le comportement n'est pas défini. Pour les opérandes intégraux, l' /opérateur donne le quotient algébrique avec toute partie fractionnaire rejetée. si le quotient a/best représentable dans le type du résultat, (a/b)*b + a%best égal à a.

ÉDITER:

De nombreuses questions ont été soulevées concernant la lisibilité et la maintenabilité d'une construction comme celle-ci. La réponse donnée par motoDrizzt est un bon exemple de simplification de la construction originale qui est plus maintenable et n'est pas aussi «moche».

En développant sa réponse, voici un autre exemple utilisant l'opérateur ternaire. Étant donné que chaque cas dans l'article d'origine est assigné à la même variable, l'utilisation de cet opérateur peut aider à améliorer encore la lisibilité.

int angle = ((this->_car.getAbsoluteAngle() % 360) + 360) % 360;

this->_car.edir = (angle <= 30)  ?  Car::EDirection::RIGHT :
                  (angle <= 60)  ?  Car::EDirection::UP_RIGHT :
                  (angle <= 120) ?  Car::EDirection::UP :
                  (angle <= 150) ?  Car::EDirection::UP_LEFT :
                  (angle <= 210) ?  Car::EDirection::LEFT : 
                  (angle <= 240) ?  Car::EDirection::DOWN_LEFT :
                  (angle <= 300) ?  Car::EDirection::DOWN:  
                  (angle <= 330) ?  Car::EDirection::DOWN_RIGHT :
                                    Car::EDirection::RIGHT;

49

Ce code n'est pas moche, il est simple, pratique, lisible et facile à comprendre. Il sera isolé dans sa propre méthode, donc personne n'aura à s'en occuper dans la vie de tous les jours. Et juste au cas où quelqu'un devrait le vérifier - peut-être parce qu'il débogue votre application pour un problème ailleurs - c'est tellement facile qu'il lui faudra deux secondes pour comprendre le code et ce qu'il fait.

Si je faisais un tel débogage, je serais heureux de ne pas avoir à passer cinq minutes à essayer de comprendre ce que fait votre fonction. À cet égard, toutes les autres fonctions échouent complètement, car elles changent une routine simple, sans bogues et sans bogues, dans un désordre compliqué que les personnes lors du débogage seront obligées d'analyser et de tester en profondeur. En tant que chef de projet moi-même, je serais fortement contrarié par un développeur prenant une tâche simple et au lieu de l'implémenter de manière simple et inoffensive, je perdrais du temps à l'implémenter de manière trop compliquée. Pensez simplement à tout le temps que vous avez perdu à y penser, puis à vous adresser à SO demander, et tout cela dans le seul souci de détériorer la maintenance et la lisibilité de la chose.

Cela dit, il y a une erreur courante dans votre code qui le rend beaucoup moins lisible, et quelques améliorations que vous pouvez faire assez facilement:

int angle = this->_car.getAbsoluteAngle();

if (angle <= 30 || angle >= 330)
  return Car::EDirection::RIGHT;
else if (angle <= 60)
  return Car::EDirection::UP_RIGHT;
else if (angle <= 120)
  return Car::EDirection::UP;
else if (angle <= 150)
  return Car::EDirection::UP_LEFT;
else if (angle <= 210)
  return Car::EDirection::LEFT;
else if (angle <= 240)
  return Car::EDirection::DOWN_LEFT;
else if (angle <= 300)
  return Car::EDirection::DOWN;
else if (angle <= 330)
  return Car::EDirection::DOWN_RIGHT;

Mettez ceci dans une méthode, affectez la valeur renvoyée à l'objet, réduisez la méthode et oubliez-la pour le reste de l'éternité.

PS il y a un autre bogue au-dessus du seuil 330, mais je ne sais pas comment vous voulez le traiter, donc je ne l'ai pas corrigé du tout.


Mise à jour ultérieure

Selon le commentaire, vous pouvez même vous débarrasser de l'autre, voire pas du tout:

int angle = this->_car.getAbsoluteAngle();

if (angle <= 30 || angle >= 330)
  return Car::EDirection::RIGHT;

if (angle <= 60)
  return Car::EDirection::UP_RIGHT;

if (angle <= 120)
  return Car::EDirection::UP;

if (angle <= 150)
  return Car::EDirection::UP_LEFT;

if (angle <= 210)
  return Car::EDirection::LEFT;

if (angle <= 240)
  return Car::EDirection::DOWN_LEFT;

if (angle <= 300)
  return Car::EDirection::DOWN;

if (angle <= 330)
  return Car::EDirection::DOWN_RIGHT;

Je ne l'ai pas fait parce que j'estime qu'à un certain moment, cela devient juste une question de préférences personnelles, et la portée de ma réponse était (et est) de donner une perspective différente à votre inquiétude concernant la "laideur du code". Quoi qu'il en soit, comme je l'ai dit, quelqu'un l'a souligné dans les commentaires et je pense qu'il est logique de le montrer.


1
Qu'est-ce que c'était censé accomplir?
abstraction est tout.

8
si vous voulez emprunter cette voie, vous devez au moins vous débarrasser de l'inutile else if, ifc'est suffisant.
Ðаn

10
@ Et je suis complètement en désaccord sur le else if. Je trouve utile de pouvoir jeter un coup d'œil sur un bloc de code et de voir qu'il s'agit d'un arbre de décision plutôt que d'un groupe d'instructions non liées. Oui, elseou breakne sont pas nécessaires pour le compilateur après a return, mais ils sont utiles pour l'humain qui regarde le code.
IMSoP

@ Ðаn Je n'ai jamais vu une langue où une imbrication supplémentaire serait nécessaire. Soit il existe un mot clé elseif/ séparé elsif, soit vous utilisez techniquement un bloc à une instruction qui commence if, comme ici. Exemple rapide de ce à quoi je pense que vous pensez, et à quoi je pense: gist.github.com/IMSoP/90bc1e9e2c56d8314413d7347e76532a
IMSoP

7
@ «Oui, je suis d'accord que ce serait horrible. Mais ce n'est pas la elseraison pour laquelle vous faites cela, c'est un mauvais guide de style qui ne se reconnaît pas else ifcomme une déclaration distincte. J'utiliserais toujours des accolades, mais je n'écrirais jamais de code comme ça, comme je l'ai montré dans mon essence.
IMSoP

39

En pseudocode:

angle = (angle + 30) %360; // Offset by 30. 

Nous avons donc 0-60, 60-90, 90-150, ... comme les catégories. Dans chaque quadrant de 90 degrés, une partie en a 60, une partie en a 30. Alors, maintenant:

i = angle / 90; // Figure out the quadrant. Could be 0, 1, 2, 3 

j = (angle - 90 * i) >= 60? 1: 0; // In the quardrant is it perfect (eg: RIGHT) or imperfect (eg: UP_RIGHT)?

index = i * 2 + j;

Utilisez l'index dans un tableau contenant les énumérations dans l'ordre approprié.


7
C'est bien, probablement la meilleure réponse ici. Il y a de fortes chances que si le questionneur original regardait son utilisation de l'énumération plus tard, il trouverait qu'il a un cas où il vient d'être reconverti en un nombre! Éliminer complètement l'énumération et s'en tenir à un entier de direction a probablement du sens avec d'autres endroits dans son code et cette réponse vous y mène directement.
Bill K

18
switch (this->_car.getAbsoluteAngle() / 30) // integer division
{
    case 0:
    case 11: this->_car.edir = Car::EDirection::RIGHT; break;
    case 1: this->_car.edir = Car::EDirection::UP_RIGHT; break;
    ...
    case 10: this->_car.edir = Car::EDirection::DOWN_RIGHT; break;
}

angle = this -> _ car.getAbsoluteAngle (); secteur = (angle% 360) / 30; Le résultat est 12 secteurs. Ensuite, indexez dans le tableau, ou utilisez switch / case comme ci-dessus - quel compilateur se transforme quand même en table de saut.
ChuckCottrill

1
Switch pas vraiment mieux que les chaînes if / else.
Bill K

5
@BillK: Cela pourrait être, si le compilateur le transforme en une table de recherche pour vous. C'est plus probable qu'avec une chaîne if / else. Mais comme c'est facile et ne nécessite aucune astuce spécifique à l'architecture, il est probablement préférable d'écrire la recherche de table dans le code source.
Peter Cordes

En général, les performances ne devraient pas être un problème - c'est la lisibilité et la maintenabilité - chaque commutateur et chaîne if / else signifie généralement un tas de code de copier-coller désordonné qui doit être mis à jour à plusieurs endroits chaque fois que vous ajoutez un nouvel élément. Il est préférable d'éviter les deux et d'essayer de distribuer des tables, des calculs ou simplement de charger des données à partir d'un fichier et de les traiter comme des données si vous le pouvez.
Bill K

PeterCordes, le compilateur, est susceptible de générer un code identique pour la LUT et pour le commutateur. @BillK vous pouvez extraire le commutateur dans une fonction 0..12 -> Car :: EDirection, qui aurait une répétition équivalente à une LUT
Caleth

16

Ignorant votre premier ifqui est un peu un cas particulier, les autres suivent tous exactement le même schéma: un min, un max et une direction; pseudo-code:

if (angle > min && angle <= max)
  _car.edir = direction;

Faire de ce vrai C ++ pourrait ressembler à:

enum class EDirection {  NONE,
   RIGHT, UP_RIGHT, UP, UP_LEFT, LEFT, DOWN_LEFT, DOWN, DOWN_RIGHT };

struct AngleRange
{
    int min, max;
    EDirection direction;
};

Maintenant, plutôt que d'écrire un tas de ifs, parcourez simplement vos différentes possibilités:

EDirection direction_from_angle(int angle, const std::vector<AngleRange>& angleRanges)
{
    for (auto&& angleRange : angleRanges)
    {
        if ((angle > angleRange.min) && (angle <= angleRange.max))
            return angleRange.direction;
    }

    return EDirection::NONE;
}

( throwUne exception plutôt returnqu'ing NONEest une autre option).

Ce que vous appelleriez alors:

_car.edir = direction_from_angle(_car.getAbsoluteAngle(), {
    {30, 60, EDirection::UP_RIGHT},
    {60, 120, EDirection::UP},
    // ... etc.
});

Cette technique est connue sous le nom de programmation basée sur les données . En plus de vous débarrasser d'un tas de ifs, cela vous permettrait d'ajouter facilement plus de directions (par exemple, NNW) ou de réduire le nombre (gauche, droite, haut, bas) sans retravailler d'autres codes.


(Le traitement de votre premier cas particulier est laissé comme "un exercice pour le lecteur" :-))


1
Techniquement, vous pouvez éliminer min étant donné que toutes les plages d'angle correspondent, ce qui réduirait la condition à if(angle <= angleRange.max)+1 pour l'utilisation de fonctionnalités C ++ 11 telles que enum class.
Pharap

12

Bien que les variantes proposées basées sur une table de recherche angle / 30soient probablement préférables, voici une alternative qui utilise une recherche binaire codée en dur pour minimiser le nombre de comparaisons.

static Car::EDirection directionFromAngle( int angle )
{
    if( angle <= 210 )
    {
        if( angle > 120 )
            return angle > 150 ? Car::EDirection::LEFT
                               : Car::EDirection::UP_LEFT;
        if( angle > 30 )
            return angle > 60 ? Car::EDirection::UP
                              : Car::EDirection::UP_RIGHT;
    }
    else // > 210
    {
        if( angle <= 300 )
            return angle > 240 ? Car::EDirection::DOWN
                               : Car::EDirection::DOWN_LEFT;
        if( angle <= 330 )
            return Car::EDirection::DOWN_RIGHT;
    }
    return Car::EDirection::RIGHT; // <= 30 || > 330
}

2

Si vous voulez vraiment éviter la duplication, vous pouvez l'exprimer sous forme de formule mathématique.

Tout d'abord, supposons que nous utilisons Enum de @ Geek

Enum EDirection { RIGHT =0, UP_RIGHT, UP, UP_LEFT, LEFT, DOWN_LEFT,DOWN, DOWN_RIGHT}

Nous pouvons maintenant calculer l'énumération en utilisant des mathématiques entières (sans avoir besoin de tableaux).

EDirection angle2dir(int angle) {
    int d = ( ((angle%360)+360)%360-1)/30;
    d-=d/3; //some directions cover a 60 degree arc
    d%=8;
    //printf ("assert(angle2dir(%3d)==%-10s);\n",angle,dir2str[d]);
    return (EDirection) d;
}

Comme le souligne @motoDrizzt, un code concis n'est pas nécessairement un code lisible. Cela présente le petit avantage de l'exprimer sous forme de mathématiques, ce qui explique clairement que certaines directions couvrent un arc plus large. Si vous voulez aller dans cette direction, vous pouvez ajouter des assertions pour aider à comprendre le code.

assert(angle2dir(  0)==RIGHT     ); assert(angle2dir( 30)==RIGHT     );
assert(angle2dir( 31)==UP_RIGHT  ); assert(angle2dir( 60)==UP_RIGHT  );
assert(angle2dir( 61)==UP        ); assert(angle2dir(120)==UP        );
assert(angle2dir(121)==UP_LEFT   ); assert(angle2dir(150)==UP_LEFT   );
assert(angle2dir(151)==LEFT      ); assert(angle2dir(210)==LEFT      );
assert(angle2dir(211)==DOWN_LEFT ); assert(angle2dir(240)==DOWN_LEFT );
assert(angle2dir(241)==DOWN      ); assert(angle2dir(300)==DOWN      );
assert(angle2dir(301)==DOWN_RIGHT); assert(angle2dir(330)==DOWN_RIGHT);
assert(angle2dir(331)==RIGHT     ); assert(angle2dir(360)==RIGHT     );

Après avoir ajouté les assertions, vous avez ajouté la duplication, mais la duplication dans les assertions n'est pas si grave. Si vous avez une affirmation incohérente, vous le saurez assez tôt. Les assertions peuvent être compilées hors de la version finale afin de ne pas gonfler l'exécutable que vous distribuez. Néanmoins, cette approche est probablement la plus applicable si vous souhaitez optimiser le code plutôt que simplement le rendre moins laid.


1

Je suis en retard à la fête, mais nous pourrions utiliser des indicateurs d'énumération et des vérifications de plage pour faire quelque chose de soigné.

enum EDirection {
    RIGHT =  0x01,
    LEFT  =  0x02,
    UP    =  0x04,
    DOWN  =  0x08,
    DOWN_RIGHT = DOWN | RIGHT,
    DOWN_LEFT = DOWN | LEFT,
    UP_RIGHT = UP | RIGHT,
    UP_LEFT = UP | LEFT,

    // just so we be clear, these won't have much use though
    IMPOSSIBLE_H = RIGHT | LEFT, 
    IMPOSSIBLE_V = UP | DOWN
};

la vérification (pseudo-code), en supposant que l'angle est absolu (entre 0 et 360):

int up    = (angle >   30 && angle <  150) * EDirection.UP;
int down  = (angle >  210 && angle <  330) * EDirection.DOWN;
int right = (angle <=  60 || angle >= 330) * EDirection.Right;
int left  = (angle >= 120 && angle <= 240) * EDirection.LEFT;

EDirection direction = (Direction)(up | down | right | left);

switch(direction){
    case RIGHT:
         // do right
         break;
    case UP_RIGHT:
         // be honest
         break;
    case UP:
         // whats up
         break;
    case UP_LEFT:
         // do you even left
         break;
    case LEFT:
         // 5 done 3 to go
         break;
    case DOWN_LEFT:
         // your're driving me to a corner here
         break;
    case DOWN:
         // :(
         break;
    case DOWN_RIGHT:
         // completion
         break;

    // hey, we mustn't let these slide
    case IMPOSSIBLE_H:
    case IMPOSSIBLE_V:
        // treat your impossible case here!
        break;
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.