Pourquoi Clang / LLVM m’avertit-il de l’utilisation de default dans une instruction switch où tous les cas énumérés sont couverts?

34

Considérez l'énumération suivante et l'instruction switch:

typedef enum {
    MaskValueUno,
    MaskValueDos
} testingMask;

void myFunction(testingMask theMask) {
    switch (theMask) {
        case MaskValueUno: {}// deal with it
        case MaskValueDos: {}// deal with it
        default: {} //deal with an unexpected or uninitialized value
    }
};

Je suis un programmeur Objective-C, mais je l'ai écrit en C pur pour un public plus large.

Clang / LLVM 4.1 avec -Weverything me met en garde sur la ligne par défaut:

Label par défaut dans le commutateur qui couvre toutes les valeurs d'énumération

Maintenant, je peux en quelque sorte comprendre pourquoi c'est là: dans un monde parfait, les seules valeurs entrant dans l'argument theMaskseraient dans l'énumération, aucun paramètre par défaut n'est nécessaire. Mais que se passe-t-il si un piratage arrive et jette un int non initialisé dans ma belle fonction? Ma fonction sera remplacée par une bibliothèque vide et je n’ai aucun contrôle sur ce qui pourrait y entrer. L'utilisation defaultest une manière très soignée de gérer cela.

Pourquoi les dieux de la LLVM estiment-ils que ce comportement est indigne de leur appareil infernal? Devrais-je être précédé d'une instruction if pour vérifier l'argument?

Swizzlr
la source
1
Je devrais dire, ma raison pour -Weverything est de me faire un meilleur programmeur. Comme le dit NSHipster : "Pro tip: Try setting the -Weverything flag and checking the “Treat Warnings as Errors” box your build settings. This turns on Hard Mode in Xcode.".
Swizzlr
2
-WeverythingCela peut être utile, mais veillez à ne pas trop muter votre code pour pouvoir le gérer. Certains de ces avertissements ne sont pas seulement inutiles, ils sont contre-productifs et il vaut mieux les désactiver. (En effet, c'est le cas d'utilisation de -Weverything: commencez par l'activer et désactivez ce qui n'a pas de sens.)
Steven Fisher
Pourriez-vous développer un peu le message d'avertissement pour la postérité? Il y a généralement plus que cela.
Steven Fisher
Après avoir lu les réponses, je préfère toujours votre solution initiale. Utilisation de la déclaration par défaut IMHO est meilleure que les alternatives données dans les réponses. Juste une petite note: les réponses sont vraiment bonnes et informatives, elles montrent de manière très cool comment résoudre le problème.
bbaja42
@StevenFisher C'était tout l'avertissement. Et comme Killian l'a fait remarquer, si je modifiais mon enum plus tard, cela ouvrirait la possibilité à des valeurs valides de passer à l'implémentation par défaut. Cela semble être un bon modèle de conception (si c'est une telle chose).
Swizzlr

Réponses:

31

Voici une version qui ne souffre ni de la signalisation du problème ni de celle contre laquelle vous vous protégez:

void myFunction(testingMask theMask) {
    assert(theMask == MaskValueUno || theMask == MaskValueDos);
    switch (theMask) {
        case MaskValueUno: {}// deal with it
        case MaskValueDos: {}// deal with it
    }
}

Killian a déjà expliqué pourquoi clang émet l’avertissement: si vous étendiez l’énum, ​​vous tomberiez dans le cas par défaut, ce qui n’est probablement pas ce que vous voulez. La bonne chose à faire est de supprimer le cas par défaut et d'obtenir des avertissements pour les conditions non gérées .

Vous craignez maintenant que quelqu'un puisse appeler votre fonction avec une valeur en dehors de l'énumération. Cela ressemble à un non-respect de la condition préalable de la fonction: il est documenté d'attendre une valeur de l' testingMaskénumération, mais le programmeur a passé quelque chose d'autre. Faites donc une erreur de programmation en utilisant assert()(ou NSCAssert()comme vous avez dit que vous utilisez Objective-C). Faites que votre programme plante avec un message expliquant que le programmeur le fait mal, si le programmeur le fait mal.


la source
6
+1 Comme une petite modification, je préfère avoir chaque cas return;et ajouter un assert(false);à la fin (au lieu de me répéter en énumérant les énumérations légales dans une initiale assert()et dans la switch).
Josh Kelley
1
Que se passe-t-il si l'énumération incorrecte n'est pas passée par un programmeur stupide, mais par un bug de corruption de mémoire? Lorsque le pilote appuie sur la pause de sa Toyota et qu'un bogue a corrompu l'énumération, le programme de gestion de la rupture doit planter et graver, et un texte devrait s'afficher: "le programmeur le fait mal, mauvais programmeur! ". Je ne vois pas très bien en quoi cela aiderait l'utilisateur avant qu'il ne franchisse le bord d'une falaise.
2
@ Lundin, c’est ce que fait le PO ou s’agit-il d’un cas théorique inutile que vous venez de construire? Quoi qu’il en soit, un "bug de corruption de la mémoire" [i] est une erreur du programmeur, [ii] n’est pas une chose à partir de laquelle vous pouvez continuer de manière significative (du moins pas avec les exigences énoncées dans la question).
1
@GrahamLee Je dis simplement que "se coucher et mourir" n'est pas nécessairement la meilleure option lorsque vous décelez une erreur inattendue.
1
Le nouveau problème est que vous pourriez laisser l’assertion et les cas se désynchroniser par accident.
user253751
9

Avoir une defaultétiquette ici est un indicateur que vous êtes confus quant à ce que vous attendez. Depuis que tu as épuisé tous les possiblesenum explicitement valeurs , il defaultest impossible de l'exécuter et vous n'avez pas non plus besoin de cette option pour vous protéger des modifications futures, car si vous étiez étendu enum, la construction générerait déjà un avertissement.

Ainsi, le compilateur remarque que vous avez couvert toutes les bases mais semble penser que vous ne l’avez pas fait, ce qui est toujours un mauvais signe. En faisant un effort minimal pour modifier le switchformulaire comme prévu, vous montrez au compilateur que ce que vous semblez faire est ce que vous êtes en train de faire, et vous le savez.

Kilian Foth
la source
1
Mais ma préoccupation est une variable sale et une protection contre cela (comme, comme je l'ai dit, un int non initialisé). Il me semble qu'il serait possible que l'instruction switch atteigne le défaut dans une telle situation.
Swizzlr
Je ne connais pas la définition d'Objective-C par cœur, mais j'ai supposé qu'une énumération typdef'd serait appliquée par le compilateur. En d'autres termes, une valeur "non initialisée" ne peut entrer dans la méthode que si votre programme présente déjà un comportement indéfini, et je trouve tout à fait raisonnable qu'un compilateur n'en tienne pas compte.
Kilian Foth
3
@KilianFoth non, ils ne sont pas. Les énumérations Objective-C sont des énumérations, pas des énumérations Java. Toute valeur du type intégral sous-jacent peut être présente dans l'argument de la fonction.
2
De plus, les énumérations peuvent être définies au moment de l'exécution, à partir d'entiers. Ils peuvent donc contenir n'importe quelle valeur imaginable.
OP est correct. testingMask m; maFonction (m); serait probablement frappé le cas par défaut.
Matthew James Briggs
4

Clang est confus, ayant une déclaration par défaut, il y a une pratique parfaite, on l'appelle correcte de programmation défensive et est considérée comme une bonne pratique de programmation (1). Il est largement utilisé dans les systèmes critiques, mais peut-être pas dans la programmation bureautique.

La programmation défensive a pour but de détecter des erreurs inattendues qui, en théorie, ne se produiraient jamais. Une telle erreur inattendue n’est pas nécessairement le programmeur qui donne à la fonction une entrée incorrecte, ni même un "piratage diabolique". Plus probablement, cela pourrait être dû à une variable corrompue: débordement de tampon, débordement de pile, code de bourrage et bogues similaires non liés à votre fonction pourraient en être la cause. Et dans le cas de systèmes embarqués, les variables peuvent éventuellement changer en raison de l'EMI, en particulier si vous utilisez des circuits de RAM externes.

En ce qui concerne ce qui est écrit dans la déclaration par défaut ... si vous soupçonnez que le programme a mal tourné une fois que vous y êtes arrivé, vous avez besoin d’une sorte de traitement des erreurs. Dans de nombreux cas, vous pouvez probablement simplement simplement ajouter une déclaration vide avec un commentaire: "inattendu mais ça ne fait rien", etc., pour montrer que vous avez pensé à la situation improbable.


(1) MISRA-C: 2004 15.3.


la source
1
N'oubliez pas que le programmeur PC moyen trouve généralement le concept de programmation défensive complètement étranger, car sa vision d'un programme est une utopie abstraite où rien ne peut être erroné en théorie ne va mal se passer dans la pratique. Par conséquent, vous obtiendrez des réponses très diverses selon votre question.
3
Clang n'est pas confus; il a été explicitement demandé de fournir tous les avertissements, y compris ceux qui ne sont pas toujours utiles, tels que les avertissements stylistiques. (Personnellement, j’ai opté pour cet avertissement car je ne veux pas de paramètres par défaut dans mes commutateurs d’énumération; cela signifie que je ne reçois pas d’avertissement si j’ajoute une valeur d’énum sans le gérer. Pour cette raison, je gère toujours les mauvaise affaire en dehors de l'énum.)
Jens Ayton
2
@ JensAyton C'est confus. Tous les outils d'analyse statiques professionnels, ainsi que tous les compilateurs conformes à MISRA, émettent un avertissement si une instruction switch n'a pas d' instruction par défaut. Essayez-en un vous-même.
4
Le codage dans MISRA-C n’est pas la seule utilisation valable d’un compilateur C et, encore une fois, -Weverythingactive tous les avertissements, pas une sélection appropriée à un cas d’utilisation particulier. Ne correspond pas à votre goût n'est pas la même chose que la confusion.
Jens Ayton
2
@Lundin: Je suis presque sûr que s'en tenir à MISRA-C ne crée pas comme par magie des programmes sûrs et sans bogues ... entendu parler de MISRA. En fait, ce n'est guère plus que l'opinion de quelqu'un (populaire, mais pas incontestée), et il y a des gens qui trouvent certaines de ses règles arbitraires et parfois même nuisibles en dehors du contexte pour lequel elles ont été conçues.
cHao
4

Mieux encore:

typedef enum {
    MaskValueUno,
    MaskValueDos,

    MaskValue_count
} testingMask;

void myFunction(testingMask theMask) {
    assert(theMask >= 0 && theMask<MaskValue_count);
    switch theMask {
        case MaskValueUno: {}// deal with it
        case MaskValueDos: {}// deal with it
    }
};

Cela est moins sujet aux erreurs lors de l'ajout d'éléments à l'énumération. Vous pouvez ignorer le test pour> = 0 si vous définissez vos valeurs enum non signées. Cette méthode ne fonctionne que si vos valeurs enum ne manquent pas, mais c'est souvent le cas.

Steve Weller
la source
2
Cela pollue le type avec des valeurs que vous ne voulez tout simplement pas contenir. Il a des similitudes avec le bon vieux WTF ici: thedailywtf.com/articles/What_Is_Truth_0x3f_
Martin Sugioarto 21/09/2016
4

Mais que se passe-t-il si un piratage arrive et jette un int non initialisé dans ma belle fonction?

Ensuite, vous obtenez un comportement indéfini , et votredefault volonté n'aura aucun sens. Vous ne pouvez rien faire pour améliorer la situation.

Laissez-moi être plus clair. Le moment où quelqu'un passe un non initialisé intdans votre fonction, c'est un comportement indéfini. Votre fonction pourrait résoudre le problème de l’arrêt et cela n’aurait aucune importance. C'est UB. Vous ne pouvez rien faire une fois que UB a été appelé.

DeadMG
la source
3
Que diriez-vous de le journaliser ou de lancer une exception à la place si vous l'ignorez en silence?
Jgauffin
3
En effet, beaucoup de choses peuvent être faites, telles que ... l'ajout d'une instruction par défaut au commutateur et la gestion en douceur de l'erreur. Ceci est connu sous le nom de programmation défensive . Cela protégera non seulement le programmeur idiot qui manipule la fonction avec des entrées incorrectes, mais également divers bugs causés par un débordement de tampon, un débordement de pile, un code
1
Non, ça ne gèrera rien. C'est UB. Le programme a UB au moment où la fonction est entrée et le corps de la fonction ne peut rien y faire.
DeadMG
2
@DeadMG Une énumération peut avoir n'importe quelle valeur que son type entier correspondant dans l'implémentation pourrait avoir. Il n'y a rien d'indéfini à ce sujet. L'accès au contenu d'une variable automatique non initialisée est bien UB, mais le "piratage maléfique" (qui est plus vraisemblablement une sorte de bogue) pourrait tout aussi bien jeter une valeur bien définie à la fonction, même si ce n'est pas l'une des valeurs énumérés dans la déclaration enum.
2

La déclaration par défaut ne serait pas nécessairement utile. Si le commutateur est sur une énumération, toute valeur non définie dans la énumération finira par exécuter un comportement non défini.

Pour tout ce que vous savez, le compilateur peut compiler ce commutateur (avec la valeur par défaut) comme suit:

if (theMask == MaskValueUno)
  // Execute something MaskValueUno code
else // theMask == MaskValueDos
  // Execute MaskValueDos code

Une fois que vous avez déclenché un comportement indéfini, vous ne pouvez plus revenir en arrière.

Moi même
la source
2
Tout d'abord, c'est une valeur non spécifiée , pas un comportement indéfini. Cela signifie que le compilateur peut assumer une autre valeur plus pratique, mais ne transformera pas la lune en fromage vert, etc. Deuxièmement, pour autant que je sache, cela ne s'applique qu'au C ++. En C, la plage de valeurs d'un enumtype est identique à la plage de valeurs de son type entier sous-jacent (défini par l'implémentation, il doit donc être autocohérent).
Jens Ayton
1
Cependant, une instance d'une variable enum et une constante d'énumération ne doivent pas nécessairement avoir la même largeur et la même signature, elles sont toutes deux impl.defined. Mais je suis à peu près sûr que toutes les énumérations de C sont évaluées exactement comme leur type entier, il n’ya donc pas de comportement indéfini ni même indéterminé.
2

Je préfère aussi avoir un default:dans tous les cas. Je suis en retard à la fête comme d'habitude, mais ... d'autres pensées que je n'ai pas vues plus haut:

  • L'avertissement particulier (ou l'erreur si aussi lancer -Werror) provient de -Wcovered-switch-default(de -Weverythingmais pas -Wall). Si votre souplesse morale vous permet de désactiver certains avertissements (c.-à-d. Exciser certaines choses de -Wallou -Weverything), envisagez de lancer -Wno-covered-switch-default(ou de les -Wno-error=covered-switch-defaultutiliser -Werror), et en général -Wno-...pour d'autres avertissements que vous trouvez désagréables.
  • Pour gcc(et un comportement plus générique clang), consultez la gccpage de manuel pour -Wswitch, -Wswitch-enum, -Wswitch-defaultpour (différent) comportement dans des situations similaires des types énumérés dans les états de commutation.
  • Je n'aime pas non plus cet avertissement conceptuel, ni son libellé; pour moi, les mots de l'avertissement ("étiquette par défaut ... couvre toutes les ... valeurs") suggèrent que le default:cas sera toujours exécuté, tel que

    switch (foo) {
      case 1:
        do_something(); 
        //note the lack of break (etc.) here!
      default:
        do_default();
    }

    En première lecture, ce que je pensais que vous étiez en cours d' exécution dans - que votre default:cas serait toujours exécuté parce qu'il n'y a pas break;ou return;ou similaire. Ce concept est similaire (à mon oreille) à un autre commentaire (bien qu’exceptionnellement utile) de la part d’une nounou clang. Si foo == 1, les deux fonctions seront exécutées; votre code ci-dessus a ce comportement. C'est-à-dire que vous ne pouvez vous échapper que si vous souhaitez continuer à exécuter le code des cas suivants! Cela ne semble toutefois pas être votre problème.

Au risque d’être pédant, quelques autres réflexions pour la complétude:

  • Je pense cependant que ce comportement est (plus) compatible avec la vérification de type agressive dans d’autres langages ou compilateurs. Si, comme vous le supposez, un reproche fait tentent de passer une intou quelque chose à cette fonction, qui est explicitement l' intention de consommer votre propre type spécifique, votre compilateur doit vous protéger aussi bien dans cette situation avec un avertissement agressif ou erreur. MAIS ça ne marche pas! (C'est-à-dire, il semble qu'au moins gccet clangne fait pas de enumvérification de type, mais j'ai entendu dire que iccoui ). Étant donné que vous n'obtenez pas la sécurité de type , vous pouvez obtenir une sécurité de valeur, comme indiqué ci-dessus. Sinon, comme suggéré dans TFA, considérez un structou quelque chose qui peut fournir une sécurité de type.
  • Une autre solution de contournement pourrait être de créer une nouvelle "valeur" dans votre enumtype MaskValueIllegal, et de ne pas soutenir cela casedans votre switch. Cela serait mangé par le default:(en plus de toute autre valeur farfelue)

Vive le codage défensif!

hoc_age
la source
1

Voici une suggestion alternative:
Le PO essaie de se protéger contre le cas où quelqu'un passe dans un intendroit où une énumération est attendue. Ou plus probablement, lorsque quelqu'un a lié une ancienne bibliothèque à un programme plus récent en utilisant un en-tête plus récent avec plus de cas.

Pourquoi ne pas changer le commutateur pour gérer le intcas? Ajouter une distribution devant la valeur dans le commutateur élimine l'avertissement et fournit même un indice sur la raison pour laquelle la valeur par défaut existe.

void myFunction(testingMask theMask) {
    int maskValue = int(theMask);
    switch(maskValue) {
        case MaskValueUno: {} // deal with it
        case MaskValueDos: {}// deal with it
        default: {} //deal with an unexpected or uninitialized value
    }
}

Je trouve cela beaucoup moins désagréable que de assert()tester chacune des valeurs possibles ou même de supposer que la plage de valeurs enum est bien ordonnée, de sorte qu'un test plus simple fonctionne. Ceci est juste une façon laide de faire ce que défaut fait précisément et magnifiquement.

Richard
la source
1
Cet article est plutôt difficile à lire (mur de texte). Pourriez - vous l' esprit modifier ing dans une meilleure forme?
moucher le