7
votes

Faire en sorte que le compilateur suppose que tous les cas sont traités dans le commutateur sans valeur par défaut

Commençons par un peu de code. Ceci est une version extrêmement simplifiée de mon programme.

./test.cpp: In function ‘void updateColor(uint8_t)’:
./test.cpp:20:25: warning: ‘colorData’ may be used uninitialized in this function [-Wmaybe-uninitialized]
     dummyColorRecepient = colorData;
     ~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~

Le programme se compile avec un avertissement:

#include <stdint.h>

volatile uint16_t dummyColorRecepient;

void updateColor(const uint8_t iteration)
{
    uint16_t colorData;
    switch(iteration)
    {
    case 0:
        colorData = 123;
        break;
    case 1:
        colorData = 234;
        break;
    case 2:
        colorData = 345;
        break;
    }
    dummyColorRecepient = colorData;
}

// dummy main function
int main()
{
    uint8_t iteration = 0;
    while (true)
    {
        updateColor(iteration);
        if (++iteration == 3)
            iteration = 0;
    }
}

Comme vous pouvez le voir, il y a une certitude absolue que la variable itération est toujours 0 , 1 ou 2 . Cependant, le compilateur ne le sait pas et suppose que le commutateur ne peut pas initialiser colorData . (Toute analyse statique lors de la compilation n'aidera pas ici car le programme réel est réparti sur plusieurs fichiers.)

Bien sûr, je pourrais simplement ajouter une instruction par défaut, comme default: colorData = 0 ; mais cela ajoute 24 octets supplémentaires au programme. Ceci est un programme pour un microcontrôleur et j'ai des limites très strictes pour sa taille.

Je voudrais informer le compilateur que ce commutateur est garanti pour couvrir toutes les valeurs possibles de itération .


12 commentaires

Pourquoi est ce que tu veux faire ça? Vous pouvez désactiver l'avertissement globalement ou localement pour cette instruction de commutateur unique


Pouvez-vous simplement utiliser dummyColorRecepient au lieu de colorData


Utilisez une énumération au lieu de nombres magiques ?


lancer une exception dans le cas du commutateur par défaut. Si la définition est modifiée, toutes les fonctions qui l'utilisent doivent également l'être.


@drescherjm @Someprogrammerdude Comme je l'ai écrit, ce n'est qu'un exemple très simplifié. Dans le programme réel, il y a beaucoup d'autres opérations, les nombres sont générés de manière plus compliquée et dummyColorRecepient est une fonction qui envoie les données.


@ user463035818 Je veux m'assurer que le compilateur ne considérera pas cela comme un comportement indéfini.


@Neijwiert Comment les exceptions fonctionnent-elles même sur un microcontrôleur? Cela ne générerait-il pas beaucoup de code supplémentaire?


il n'y a aucun moyen pour le compilateur de considérer cela comme un comportement indéfini. C'est juste un avertissement, c'est-à-dire qu'il s'agit potentiellement d'un bogue, mais ce n'est pas le cas dans ce cas


Je m'attendrais à ce que l'exception soit plus de code que le par défaut: colorData = 0;


Ce programme particulier, en supposant que rien d'autre n'y est lié, n'a en effet pas de comportement indéfini. C'est l'hypothèse qui vous obtiendra. Je me demande si le fait de donner la fonction de liaison interne pourrait aider ... (modifier: non, non)


Enum ne fonctionnera pas car cette variable est utilisée d'une manière différente si dans certains cas. (Je me suis assuré qu'il est remis à 0 avant de commencer à utiliser cette fonction.)


@NO_NAME une énumération fonctionnera. Définissez une énumération pour toutes les valeurs possibles, puis demandez simplement à la fonction appelante de convertir sa valeur de données en type enum lors de la transmission à cette fonction, puis ayez un case pour chaque valeur d'énumération. Le compilateur verra que toutes les valeurs sont traitées. L'alternative consiste à utiliser itération comme clé dans une table de recherche ou std :: map pour obtenir la valeur colorData , ou utiliser itération dans un calcul fixe, et débarrassez-vous complètement du commutateur .


5 Réponses :


5
votes

Utilisez l'idiome "expression lambda immédiatement invoquée" et un assert :

uint16_t getColorData(const uint8_t iteration)
{
    switch(iteration)
    {
        case 0: return 123;
        case 1: return 234;
    }

    assert(iteration == 2);
    return 345;
}

void updateColor(const uint8_t iteration)
{
    const uint16_t colorData = getColorData(iteration);
    dummyColorRecepient = colorData;
}
  • L'expression lambda vous permet de marquer colorData comme const . Les variables const doivent toujours être initialisées.

  • La combinaison d'instructions assert + return vous permet d'éviter les avertissements et de gérer tous les cas possibles.

  • assert n'est pas compilé en mode version, ce qui évite les frais généraux.


Vous pouvez également factoriser la fonction:

void updateColor(const uint8_t iteration)
{
    const auto colorData = [&]() -> uint16_t
    {
        switch(iteration)
        {
            case 0: return 123;
            case 1: return 234;
        }

        assert(iteration == 2);
        return 345;
    }();

    dummyColorRecepient = colorData;
}


17 commentaires

cela ne générerait-il pas peut-être un retour sans avertissement de valeur de retour? Ou est-ce un cas particulier lambda?


Pourquoi cela aide-t-il?


@appleapple En termes de taille de code, elle devrait être plus petite (peut-être) - elle ne sera certainement pas plus grande, et l'assertion piégera les bogues en mode débogage, mieux que le programme original.


@LightnessRacesinOrbit cela ne fait pas beaucoup de différence avec un cas par défaut pour cas 2: et assert (itération <2)


@appleapple: ils ont une signification complètement différente. De plus, l'assertion n'est pas compilée en dehors du mode débogage.


@VittorioRomeo Je ne vois pas la différence, pouvez-vous expliquer plus?


Qu'en est-il de default: assert (false) ?


@Artyer avertira toujours dans la version. En fait, même avec l'affirmation la plus probable.


Cela me rappelle une question de la fin de l'année dernière et je pense qu'il y avait était un moyen d'affirmer que le compilateur "comprenait" pour un objectif comme celui-ci. Mais aucune idée d'où ça s'est passé


Je pense qu'il existe une sorte de déclaration assume spécifique au compilateur.


@appleapple: default est une branche supplémentaire. assert est un "contrôle de cohérence" de débogage uniquement. C'est comme comparer des pommes et des oranges.


@LightnessRacesinOrbit __builtin_unreachable pour g ++ (Je ne suis pas sûr des autres compilateurs)


@Artyer C'est g ++. (avr-g ++ pour être précis) __builtin_unreachable semble bon. Pouvez-vous écrire une réponse?


@VittorioRomeo et bien je veux dire quelque chose comme {assert (...); switch {case 0: {...} case 1: {...} default: / * case2: * / ...}}


@Artyer Oi oi, c'est le blaireau!


L'idée générale de remplacer le dernier cas par default est bonne, même si cela diminue la lisibilité. Je pourrais l'utiliser si je ne trouve aucun autre moyen qui laisse mon intention claire.


@NO_NAME Vous pouvez également faire passer le dernier cas à default (ou vice versa), avec un commentaire approprié.



10
votes

Comme vous pouvez le voir, il y a une certitude absolue que l'itération de la variable est toujours 0, 1 ou 2.

Du point de vue de la chaîne d'outils, ce n'est pas vrai. Vous pouvez appeler cette fonction depuis un autre endroit, même depuis une autre unité de traduction. Le seul endroit où votre contrainte est appliquée est dans main , et même là, c'est fait de telle manière que pourrait être difficile pour le compilateur de raisonner. P >

Pour nos besoins, cependant, prenons pour lecture que vous n'allez pas lier d'autres unités de traduction, et que nous voulons en informer la chaîne d'outils. Eh bien, heureusement, nous pouvons!

Si cela ne vous dérange pas d'être non portable, alors il y a __builtin_unreachable built-in de GCC pour l'informer que le cas default ne devrait pas être atteint, et devrait être considéré comme inaccessible. Mon GCC est assez intelligent pour savoir que cela signifie que colorData ne sera jamais laissé non initialisé à moins que tous les paris ne soient de toute façon ouverts.

#include <stdint.h>

volatile uint16_t dummyColorRecepient;

void updateColor(const uint8_t iteration)
{
    uint16_t colorData;
    switch(iteration)
    {
    case 0:
        colorData = 123;
        break;
    case 1:
        colorData = 234;
        break;
    case 2:
        colorData = 345;
        break;

    // Comment out this default case to get the warnings back!
    default:
        __builtin_unreachable();
    }
    dummyColorRecepient = colorData;
}

// dummy main function
int main()
{
    uint8_t iteration = 0;
    while (true)
    {
        updateColor(iteration);
        if (++iteration == 3)
            iteration = 0;
    }
}

( démo en direct )

Cela n'ajoutera pas de réel par défaut , car il n'y a pas de "code" à l'intérieur. En fait, quand je l'ai branché sur Godbolt en utilisant x86_64 GCC avec -O2 , le programme était plus petit avec cet ajout que sans lui - logiquement, vous venez d'ajouter un indice d'optimisation majeur .

Il existe en fait une proposition pour en faire un attribut standard en C ++ afin qu'il puisse être une solution encore plus attrayante à l'avenir.


10 commentaires

Si avoir une valeur différente de celle spécifiée est un comportement inattendu, pourquoi ne pas lever une exception dans le cas par défaut à la place (en supposant que les exceptions sont activées)?


@Neijwiert: c'est à cela que servent les affirmations. Les exceptions peuvent être gérées.


@Neijwiert Si l'OP ne peut pas accepter la surcharge d'une autre déclaration d'affectation, je doute qu'il puisse accepter la surcharge du déroulement de la pile et de la gestion des exceptions!


Bien sûr, mes fonctions ne fonctionneront pas si je les utilise de manière incorrecte. Il s'agit d'un petit programme de type pilote autonome. Je sais exactement comment cette fonction est appelée et quelle condition sera remplie.


Mon erreur, a manqué cette partie.


@NO_NAME La langue ne le sait pas;)


@LightnessRacesinOrbit Je sais. C'est la prémisse de ma question :) J'ai besoin de savoir comment le dire.


@NO_NAME Essayez ça


__builtin_unreachable fonctionne très bien. Le programme est encore plus petit qu'avant! La vérification de la valeur 2 a probablement été optimisée. Je souhaite que ce genre de choses fasse partie de la norme, mais dans ce cas, il n'y a pas d'autre compilateur pour cette plate-forme de toute façon.


@NO_NAME Ils y travaillent :)



3
votes

Vous pouvez le faire compiler sans avertissement simplement en ajoutant une étiquette default à l'un des cas:

uint16_t colorData = 345;
switch(iteration)
{
case 0:
    colorData = 123;
    break;
case 1:
    colorData = 234;
    break;
}

Alternativement:

switch(iteration)
{
case 0:
    colorData = 123;
    break;
case 1:
    colorData = 234;
    break;
case 2: default:
    colorData = 345;
    break;
}

Essayez les deux et utilisez le plus court des deux.


3 commentaires

Ils fonctionnent tous les deux et semblent générer le même code d'assemblage. Cependant, j'aime toujours plus la réponse acceptée. Le résultat est le même et il télégraphie mon intention plus clairement.


@NO_NAME: je suis d'accord avec vous! Je viens d'essayer la réponse acceptée sur GCC pour ASM86, et elle est plus courte que mes suggestions.


Euh, c'est une solution tellement simple et facile (juste pour se débarrasser des avertissements) mais avec le recul - bonne!



1
votes

Je sais qu'il y a eu de bonnes solutions, mais alternativement Si vos valeurs vont être connues au moment de la compilation, au lieu d'une instruction switch, vous pouvez utiliser constexpr avec un modèle de fonction statique et quelques énumérateurs ; cela ressemblerait à quelque chose comme ceci dans une seule classe:

int main() {
    uint8_t input;
    std::cout << "Please enter input value [0,2]\n";
    std::cin >> input;

    auto output = ColorInfo::updateColor(input);

    std::cout << "Output: " << output << '\n';

    return 0;
}

Les instructions cout dans le if constexpr ne sont ajoutées qu'à des fins de test, mais cela devrait illustrer une autre façon possible de le faire sans avoir à utiliser une instruction switch à condition que vos valeurs soient connues au moment de la compilation. Si ces valeurs sont générées au moment de l'exécution, je ne suis pas tout à fait sûr qu'il existe un moyen d'utiliser constexpr pour obtenir ce type de structure de code, mais si c'est le cas, j'apprécierais si quelqu'un d'autre avec un peu d'expérience supplémentaire pourrait expliquer comment cela pourrait être fait avec constexpr en utilisant les valeurs de runtime . Cependant, ce code est très lisible car il n'y a pas de nombres magiques et le code est assez expressif.


- Mise à jour - p >

Après avoir lu plus d'informations sur constexpr , il est venu à mon attention qu'ils peuvent être utilisés pour générer des constantes de temps de compilation . J'ai également appris qu'ils ne peuvent pas générer de constantes d'exécution mais qu'ils peuvent être utilisés dans une fonction d'exécution . Nous pouvons prendre la structure de classe ci-dessus et l'utiliser dans une fonction d'exécution en tant que telle en ajoutant cette fonction statique à la classe:

#include <iostream>

int main() {
    auto output0 = ColorInfo::colorUpdater<ColorInfo::CR_0>();
    auto output1 = ColorInfo::colorUpdater<ColorInfo::CR_1>();
    auto output2 = ColorInfo::colorUpdater<ColorInfo::CR_2>();

    std::cout << "\n--------------------------------\n";
    std::cout << "Recipient0: " << output0 << '\n'
              << "Recipient1: " << output1 << '\n'
              << "Recipient2: " << output2 << '\n';
    return 0;
}

Cependant je veux pour changer les conventions de dénomination des deux fonctions. La première fonction que je nommerai colorUpdater () et cette nouvelle fonction que je viens de montrer ci-dessus je la nommerai updateColor () car elle semble plus intuitive de cette façon. Ainsi, la classe mise à jour ressemblera maintenant à ceci:

class ColorInfo {
public:
    enum ColorRecipient {
        CR_0 = 0,
        CR_1,
        CR_2
    };

    enum ColorType {
        CT_0 = 123,
        CT_1 = 234,
        CT_2 = 345
    };

    static uint16_t updateColor(uint8_t input) {
        if ( (input - '0') == CR_0 ) {
            return colorUpdater<CR_0>();
        }
        if ( (input - '0') == CR_1 ) {
            return colorUpdater<CR_1>();
        }
        if ( (input - '0') == CR_2 ) {
            return colorUpdater<CR_2>();
        }

        return colorUpdater<CR_0>(); // Return the default type
    }

    template<const uint8_t Iter>
    static constexpr uint16_t colorUpdater() {

        if constexpr (Iter == CR_0) {
            std::cout << "ColorData updated to: " << CT_0 << '\n';
            return CT_0;
        }

        if constexpr (Iter == CR_1) {
            std::cout << "ColorData updated to: " << CT_1 << '\n';
            return CT_1;
        }

        if constexpr (Iter == CR_2) {
            std::cout << "ColorData updated to: " << CT_2 << '\n';
            return CT_2;
        }
    }
};

Si vous voulez l'utiliser uniquement avec des constantes de temps de compilation, vous pouvez l'utiliser comme avant mais avec le nom mis à jour de la fonction.

static uint16_t colorUpdater(const uint8_t input) {
    // Don't forget to offset input due to std::cin with ASCII value.
    if ( (input - '0') == CR_0)
        return updateColor<CR_0>();
    if ( (input - '0') == CR_1)
        return updateColor<CR_1>();
    if ( (input - '0') == CR_2)
        return updateColor<CR_2>();

    return updateColor<CR_2>(); // Return the default type
}

Et si vous souhaitez utiliser ce mécanisme avec des valeurs runtime , vous pouvez simplement faire ce qui suit:

#include <iostream>

class ColorInfo {
public:
    enum ColorRecipient {
        CR_0 = 0,
        CR_1,
        CR_2
    };

    enum ColorType {
        CT_0 = 123,
        CT_1 = 234,
        CT_2 = 345
    };

    template<const uint8_t Iter>
    static constexpr uint16_t updateColor() {

        if constexpr (Iter == CR_0) {
            std::cout << "ColorData updated to: " << CT_0 << '\n';
            return CT_0;
        }

        if constexpr (Iter == CR_1) {
            std::cout << "ColorData updated to: " << CT_1 << '\n';
            return CT_1;
        }

        if constexpr (Iter == CR_2) {
            std::cout << "ColorData updated to: " << CT_2 << '\n';
            return CT_2;
        }
    }
};

int main() {
    const uint16_t colorRecipient0 = ColorInfo::updateColor<ColorInfo::CR_0>();
    const uint16_t colorRecipient1 = ColorInfo::updateColor<ColorInfo::CR_1>();
    const uint16_t colorRecipient2 = ColorInfo::updateColor<ColorInfo::CR_2>();

    std::cout << "\n--------------------------------\n";
    std::cout << "Recipient0: " << colorRecipient0 << '\n'
              << "Recipient1: " << colorRecipient1 << '\n'
              << "Recipient2: " << colorRecipient2 << '\n';

    return 0;
}


7 commentaires

Cela ne fait pas la même chose que le programme original - proposez une variante de cette solution qui peut parcourir les trois implémentations et nous parlerons;)


@LightnessRacesinOrbit Je peux aussi ajouter ceci une boucle while; Je voulais juste illustrer l'intérêt d'utiliser constexpr .


En fait, vous pouvez simplement mettre les trois dans une boucle while haha ​​tant pis


@LightnessRacesinOrbit Selon que les données d'entrée de l'OP sont à l'exécution ou à la compilation, cela déterminerait si cette structure de code fonctionnerait ou non. Je ne sais pas s'il existe un moyen d'utiliser les valeurs d'exécution avec constexpr .


Je ne sais pas pourquoi tout le monde se plaint des nombres magiques comme s'il s'agissait d'un vrai code et non d'un exemple stupide. L'affectation réelle ressemble à ceci: colorData = makeColorData (colors [0] [0], Color :: BLACK, Color :: BLACK); .


C'est un bon essai mais les valeurs de mon code sont totalement non constexpr. En fait, j'appelle la fonction updateColor à partir d'un autre fichier via un pointeur de fonction (il y a des raisons de le faire). Ayez quand même un vote favorable pour une solution originale.


@NO_NAME J'ai mis à jour ma réponse pour montrer comment elle peut être utilisée dans un contexte d'exécution.



0
votes

Eh bien, si vous êtes sûr de ne pas avoir à gérer d'autres valeurs possibles, vous pouvez simplement utiliser l'arithmétique. Élimine le branchement et la charge.

void updateColor(const uint8_t iteration)
{
    dummyColorRecepient = 123 + 111 * iteration;
}


1 commentaires

Eh bien, cela pourrait fonctionner si j'utilise des valeurs constantes dans le code réel, pas seulement dans l'exemple.