web-dev-qa-db-fra.com

Erreur / avertissement au moment de la compilation C ++ Force en cas de basculement implicite dans le commutateur

Les instructions switch peuvent être super utiles, mais conduisent à un bogue commun où un programmeur a oublié une instruction break:

switch(val) {
    case 0:
        foo();
        break;
    case 1:
        bar();
        // oops
    case 2:
        baz();
        break;
    default:
        roomba();
}

Vous n'obtiendrez évidemment aucun avertissement, car il est parfois souhaitable de recourir à une solution de secours. Un bon style de codage suggère de commenter lorsque votre erreur est délibérée, mais parfois cela est insuffisant.

Je suis sûr que la réponse à cette question est non, mais: existe-t-il un moyen actuellement (ou proposé dans le futur) de pouvoir demander au compilateur de lancer une erreur (ou au moins un avertissement!) Si votre case n'a pas au moins un des break; ou quelque chose à l'effet de // fallthru? Ce serait bien d'avoir une option de programmation défensive pour utiliser les instructions switch.

53
Barry

Eh bien, clang a -Wimplicit-fallthrough Que je ne connaissais pas mais que j'ai trouvé en utilisant -Weverything. Donc, pour ce code, il me donne l'avertissement suivant (voir en direct):

warning: unannotated fall-through between switch labels [-Wimplicit-fallthrough]
case 2:
^
note: insert '[[clang::fallthrough]];' to silence this warning
case 2:
^
[[clang::fallthrough]]; 
note: insert 'break;' to avoid fall-through
case 2:
^
break; 

La seule documentation que je peux trouver pour cet indicateur est dans le référence d'attribut qui dit:

L'attribut clang :: fallthrough est utilisé avec l'argument -Wimplicit-fallthrough pour annoter la transition intentionnelle entre les étiquettes de commutateur. Il ne peut être appliqué qu'à une instruction null placée à un point d'exécution entre n'importe quelle instruction et la prochaine étiquette de commutateur. Il est courant de marquer ces emplacements avec un commentaire spécifique, mais cet attribut est destiné à remplacer les commentaires par une annotation plus stricte, qui peut être vérifiée par le compilateur.

et fournit un exemple de la façon de marquer une transition explicite:

case 44:  // warning: unannotated fall-through
g();
[[clang::fallthrough]];
case 55:  // no warning

Cette utilisation d'un attribut pour marquer un passage explicite présente l'inconvénient de ne pas être portable. Visual Studio Génère une erreur et gcc génère l'avertissement suivant:

warning: attributes at the beginning of statement are ignored [-Wattributes]

ce qui est un problème si vous souhaitez utiliser -Werror.

J'ai essayé cela avec gcc 4.9 Et il semble que gcc ne supporte pas cet avertissement:

erreur: option de ligne de commande non reconnue '-Wimplicit-fallthrough'

Depuis GCC 7 , -Wimplicit-fallthrough Est pris en charge et __attribute__((fallthrough)) peut être utilisé pour supprimer les avertissements lorsque la chute est intentionnelle. GCC reconnaît les commentaires "fallthrough" dans certains scénarios, mais il peut être confondu assez facilement .

Je ne vois pas de moyen de générer un tel avertissement pour Visual Studio.

Remarque, Chandler Carruth explique que -Weverything N'est pas destiné à la production:

C'est un groupe fou qui permet littéralement chaque avertissement dans Clang. Ne l'utilisez pas sur votre code. Il est strictement destiné aux développeurs Clang ou pour explorer les avertissements existants.

mais il est utile pour déterminer quels avertissements existent.

Modifications de C++ 17

En C++ 17, nous obtenons l'attribut [[fallthrough]] couvert par [dcl.attr.fallthrough] p1 :

La solution de remplacement de jeton d'attribut peut être appliquée à une instruction null (9.2); une telle déclaration est une déclaration de fallthrough. La réponse au jeton d'attribut doit apparaître au plus une fois dans chaque liste d'attributs et aucune clause d'attributeargument ne doit être présente. Une instruction fallthrough ne peut apparaître que dans une instruction switch englobante (9.4.2). La prochaine instruction qui serait exécutée après une instruction fallthrough doit être une instruction étiquetée dont l'étiquette est une étiquette de cas ou une étiquette par défaut pour la même instruction switch. Le programme est mal formé s'il n'y a pas une telle déclaration.

...

[ Example:
void f(int n) {
void g(), h(), i();
switch (n) {
  case 1:
  case 2:
    g();
    [[fallthrough]];
  case 3: // warning on fallthrough discouraged
    h();
  case 4: // implementation may warn on fallthrough
    i();
    [[fallthrough]]; // ill-formed
  }
}
—end example ]

Voir exemple en direct utilisant l'attribut .

67
Shafik Yaghmour

J'écris toujours un break; avant chaque case, comme suit:

switch(val) {
    break; case 0:
        foo();
    break; case 1:
        bar();
    break; case 2:
        baz();
    break; default:
        roomba();
}

De cette façon, il est beaucoup plus évident à l'œil si un break; est manquant. La première break; est redondant je suppose, mais cela aide à être cohérent.

Ceci est une instruction switch conventionnelle, j'ai simplement utilisé les espaces blancs d'une manière différente, en supprimant la nouvelle ligne qui se trouve normalement après un break; et avant la prochaine case.

12
Aaron McDaid

Conseil: si vous mettez systématiquement une ligne vide entre les clauses de cas, l'absence de "rupture" devient plus visible pour un humain écrémant le code:

switch (val) {
    case 0:
        foo();
        break;

    case 1:
        bar();

    case 2:
        baz();
        break;

    default:
        roomba();
}

Ce n'est pas aussi efficace lorsqu'il y a beaucoup de code dans les clauses de cas individuels, mais cela a tendance à être une mauvaise odeur de code en soi.

6
zwol

Voici une réponse à la haine compulsive.

Tout d'abord, les instructions switch sont de fantastiques gotos. Ils peuvent être combinés avec d'autres flux de contrôle (célèbre, Duff's Device ), mais l'analogie évidente ici est un goto ou deux. Voici un exemple inutile:

switch (var) {
    CASE1: case 1:
        if (foo) goto END; //same as break
        goto CASE2; //same as fallthrough
    CASE2: case 2:
        break;
    CASE3: case 3:
        goto CASE2; //fall *up*
    CASE4: case 4:
        return; //no break, but also no fallthrough!
    DEFAULT: default:
        continue; //similar, if you're in a loop
}
END:

Est-ce que je recommande cela? Non. En fait, si vous envisagez cela uniquement pour annoter une erreur, votre problème est en fait autre chose.

Ce type de code le fait rendre très clair qu'une erreur peut se produire dans le cas 1, mais comme les autres bits le montrent, c'est un très puissant technique en général qui se prête également à des abus. Soyez prudent si vous l'utilisez.

Oublier un break? Eh bien, vous oublierez aussi parfois l'annotation que vous choisissez. Oublier de tenir compte de l'échec lors du changement d'une instruction switch? Tu es un mauvais programmeur. Lors de la modification des instructions switch(ou vraiment n'importe code), vous devez d'abord comprendre les.


Honnêtement, je fais très rarement ce genre d'erreur (en oubliant une pause) - certainement moins que je n'ai fait d'autres erreurs de programmation "courantes" (alias strict, par exemple). Pour être sûr, je fais actuellement (et vous recommande de le faire) simplement écrire //fallthrough, car cela clarifie au moins l'intention.

En dehors de cela, c'est juste une réalité que les programmeurs doivent accepter. Relisez votre code après l'avoir écrit et recherchez le problème occasionnel de débogage. C'est la vie.

0
imallett