web-dev-qa-db-fra.com

Constructeur par défaut de la classe d'énumération C ++ 11 définie par l'utilisateur

Existe-t-il un moyen de spécifier le constructeur par défaut d'un enum class?

J'utilise un enum class Pour spécifier un ensemble de valeurs qui sont autorisées pour un type de données particulier dans une bibliothèque: dans ce cas, ce sont les numéros d'identification des broches GPIO d'un Raspberry Pi. Cela ressemble à ceci:

enum class PinID : int {N4 = 4, N17 = 17, /* ...etc... */ }

Le but de moi de faire cela au lieu d'utiliser simplement, disons, un int est de m'assurer que le code est sûr: je peux static_assert (Ou sinon au moment de la compilation, assurez-vous que la méthode réelle utilisée est pas important pour moi) des choses comme le fait que quelqu'un n'a pas fait d'erreur d'orthographe (en passant un 5 au lieu d'un 4, etc.), et j'obtiens des messages d'erreur automatiques pour les différences de type, etc.

Le problème est alors que enum class A un constructeur par défaut qui - par souci de compatibilité avec les enums de C je suppose (car ils ont le même comportement) - s'initialise dans le enum class équivalent de 0. Dans ce cas, il n'y a pas de valeur 0. Cela signifie qu'un utilisateur faisant une déclaration/définition comme:

PinID pid = PinID();

obtient un énumérateur qui n'est pas explicitement défini (et ne semble même pas "exister" quand on regarde le code), et peut conduire à des erreurs d'exécution. Cela signifie également que des techniques comme switch sur les valeurs des énumérateurs définis explicitement sont impossibles sans avoir une erreur/cas par défaut - quelque chose que je veux éviter, car cela me force à throw ou à faire quelque chose comme retourner un boost::optional, qui se prêtent moins à l'analyse statique.

J'ai essayé de définir un constructeur par défaut en vain. J'ai (désespérément) essayé de définir une fonction qui partage le nom de enum class, Mais cela (sans surprise) a entraîné d'étranges erreurs de compilation. Je souhaite conserver la possibilité de convertir le enum class En int, avec tous les N# Énumérateurs mappant à leurs # Respectifs, donc simplement "définissant", disons , N4 = 0 est inacceptable; c'est pour la simplicité et la raison.

Je suppose que ma question est double: existe-t-il un moyen d'obtenir le type de sécurité statique que j'utilise après avoir utilisé enum class? Sinon, quelles autres possibilités préféreriez-vous? Ce que je veux, c'est quelque chose qui:

  1. est constructible par défaut
  2. peut être fait à la construction par défaut à une valeur valide arbitraire
  3. fournit le "jeu fini de valeurs spécifiées" fourni par enum class es
  4. est au moins aussi sûr pour le type qu'un enum class
  5. (de préférence) n'implique pas de polymorphisme d'exécution

La raison pour laquelle je souhaite la constructibilité par défaut est que je prévois d'utiliser boost::lexical_cast Pour réduire la surcharge syntaxique impliquée dans les conversions entre les valeurs enum class Et les strings réels associés auxquels je renvoie le système d'exploitation (sysfs dans ce cas); boost::lexical_cast Nécessite une constructibilité par défaut.

Les erreurs de raisonnement sont les bienvenues - je commence à soupçonner que enum class Es sont le bon objet pour le mauvais travail, dans ce cas; des éclaircissements seront offerts sur demande. Merci pour votre temps.

30
FizzixNerd

Un type défini avec enum class Ou enum struct N'est pas une classe mais une énumération de portée et ne peut pas avoir de constructeur par défaut défini. La norme C++ 11 définit que votre instruction PinID pid = PinID(); donnera une initialisation nulle. Où PinID était défini comme un enum class. Il permet également aux types d'énumération en général de contenir des valeurs autres que les constantes d'énumérateur.

Pour comprendre que PinID () donne une initialisation nulle, il faut lire les sections standard 3.9.9, 8.5.5, 8.5.7 et 8.5.10 ensemble:

8.5.10 - An object whose initializer is an empty set of parentheses, i.e., (), shall be value-initialized

8.5.7 - To value-initialize an object of type T means: ... otherwise, the object is zero-initialized.

8.5.5 - To zero-initialize an object or reference of type T means: — if T is a scalar type (3.9), the object is set to the value 0 (zero), taken as an integral constant expression, converted to T;

3.9.9 - Indique que les types d'énumération font partie de l'ensemble des types appelés types scalaires.

ne solution possible:

Pour répondre à vos points 1 à 5, vous pourriez écrire un cours sur le modèle de:

class PinID
{
private:
    PinID(int val)
    : m_value(val)
    {}

    int m_value;

public:
    static const PinID N4;
    static const PinID N17;
    /* ...etc... */ 

    PinID() 
    : m_value(N4.getValue())
    {}

    PinID(const PinID &id)
    : m_value(id.getValue())
    {}

    PinID &operator = (const PinID &rhs)
    {
        m_value = rhs.getValue();
        return *this;
    }

    int getValue() const
    {
        return m_value;
    }

    // Attempts to create from int and throw on failure.
    static PinID createFromInt(int i);

    friend std::istream& operator>>(std::istream &is, PinID &v)
    {
        int candidateVal(0);
        is >> candidateVal;
        v = PinID::createFromInt(candidateVal);
        return is;
    }
};

const PinID PinID::N4 = PinID(4);
/* ...etc... */

Cela peut vous donner quelque chose que vous auriez à faire des efforts spécifiques pour obtenir des valeurs non valides. Le constructeur et l'opérateur de flux par défaut devraient lui permettre de fonctionner avec lexical_cast.

Il semble que cela dépend à quel point les opérations sur un PinID sont critiques après sa création, qu'il soit utile d'écrire une classe ou de simplement gérer les valeurs non valides partout lorsque la valeur est utilisée.

16
PeterSW

Un enum class est juste un enum fortement typé; ce n'est pas un class. C++ 11 vient de réutiliser le mot clé class existant pour éviter d'introduire un nouveau mot clé qui romprait la compatibilité avec le code C++ hérité.

Quant à votre question, il n'y a aucun moyen de garantir au moment de la compilation qu'une distribution implique un bon candidat. Considérer:

int x;
std::cin >> x;
auto p = static_cast<PinID>(x);

C'est parfaitement légal et il n'y a aucun moyen de s'assurer statiquement que l'utilisateur de la console a fait la bonne chose.

Au lieu de cela, vous devrez vérifier lors de l'exécution que la valeur est valide. Pour contourner ce problème de manière automatisée, un de mes collègues a créé un générateur enum qui construit ces vérifications ainsi que d'autres routines utiles en fonction d'un fichier avec des valeurs d'énumération. Vous devrez trouver une solution qui vous convient.

4
chrisaycock

Je sais que cette question est datée et qu'elle a déjà une réponse acceptée, mais voici une technique qui pourrait aider dans une situation comme celle-ci avec certaines des nouvelles fonctionnalités de C++

Vous pouvez déclarer la variable de cette classe soit non static ou static, cela peut être fait de plusieurs manières autorisées sur le support de votre compilateur actuel.


non statique:

#include <iostream>
#include <array>

template<unsigned... IDs>
class PinIDs {
private:
    const std::array<unsigned, sizeof...(IDs)> ids { IDs... };
public:
    PinIDs() = default;
    const unsigned& operator[]( unsigned idx ) const {
        if ( idx < 0 || idx > ids.size() - 1 ) {
            return -1;
        }
        return ids[idx];
    }
};

Statique: - Il y a 3 façons d'écrire ceci: (Premier One - C++ 11 ou 14 ou supérieur) 2 derniers (c ++ 17).

Ne me citez pas sur la partie C++ 11; Je ne sais pas exactement quand les modèles variadiques ou les packs de paramètres ont été introduits pour la première fois.

template<unsigned... IDs>
class PinIDs{
private:        
    static const std::array<unsigned, sizeof...(IDs)> ids;
public:    
    PinIDs() = default;    
    const unsigned& operator[]( unsigned idx ) const {
        if ( idx < 0 || idx > ids.size() - 1 ) {
            return -1;
        }
        return ids[idx];
    }
};

template<unsigned... IDs>
const std::array<unsigned, sizeof...(IDs)> PinIDs<IDs...>::ids { IDs... };

template<unsigned... IDs>
class PinIDs{
private:
    static constexpr std::array<unsigned, sizeof...(IDs)> ids { IDs... }; 
public:   
    PinIDs() = default;    
    const unsigned& operator[]( unsigned idx ) const {
        if ( idx < 0 || idx > ids.size() - 1 ) {
            return -1;
        }
        return ids[idx];
    }
};

template<unsigned... IDs>
class PinIDs{
private:
    static inline const std::array<unsigned, sizeof...(IDs)> ids { IDs... };
public:    
    PinIDs() = default;    
    const unsigned& operator[]( unsigned idx ) const {
        if ( idx < 0 || idx > ids.size() - 1 ) {
            return -1;
        }
        return ids[idx];
    }
};

Tous les exemples ci-dessus fonctionnent de manière non statique ou statique avec le même cas d'utilisation ci-dessous et fournissent les résultats corrects:

int main() {
    PinIDs<4, 17, 19> myId;

    std::cout << myId[0] << " ";
    std::cout << myId[1] << " ";
    std::cout << myId[2] << " ";

    std::cout << "\nPress any key and enter to quit." << std::endl;
    char c;
    std::cin >> c;

    return 0;
}

Sortie

4 17 19
Press any key and enter to quit.

Avec ce type de modèle de classe utilisant une liste de paramètres variadic, vous n'avez pas besoin d'utiliser un constructeur autre que celui par défaut. J'ai ajouté des limites de vérification dans le tableau afin que le operator[] ne dépasse pas les limites de sa taille; J'ai pu lancer une erreur mais avec le type unsigned j'ai simplement renvoyé -1 comme valeur invalide.

Avec ce type, il n'y a pas de valeur par défaut car vous devez instancier ce type d'objet via une liste de paramètres de modèle avec une seule ou un ensemble de valeurs. Si l'on veut, ils peuvent specialize this class avec un seul paramètre de 0 pour un type par défaut. Lorsque vous instanciez ce type d'objet; il est définitif car il ne peut être modifié par rapport à sa déclaration. Ceci est un objet const et reste toujours constructible par défaut.

1
Francis Cugler