C ++ typedef typedef fortement

50

J'ai essayé de trouver un moyen de déclarer des typedefs fortement typés, d'attraper une certaine classe de bogues au stade de la compilation. Il arrive souvent que je tape un int dans plusieurs types d'identifiants, ou un vecteur de position ou de vélocité:

typedef int EntityID;
typedef int ModelID;
typedef Vector3 Position;
typedef Vector3 Velocity;

Cela peut rendre l'intention du code plus claire, mais après une longue nuit de codage, on peut faire des erreurs stupides comme comparer différents types d'identifiants ou ajouter une position à une vélocité.

EntityID eID;
ModelID mID;

if ( eID == mID ) // <- Compiler sees nothing wrong
{ /*bug*/ }


Position p;
Velocity v;

Position newP = p + v; // bug, meant p + v*s but compiler sees nothing wrong

Malheureusement, les suggestions que j'ai trouvées pour les types de caractères fortement typés incluent l’utilisation de boost, ce qui pour moi au moins n’est pas une possibilité (j’ai au moins le c ++ 11). Donc, après un peu de réflexion, je suis tombé sur cette idée et je voulais la faire passer à quelqu'un.

Tout d'abord, vous déclarez le type de base en tant que modèle. Le paramètre template n'est utilisé pour rien dans la définition, cependant:

template < typename T >
class IDType
{
    unsigned int m_id;

    public:
        IDType( unsigned int const& i_id ): m_id {i_id} {};
        friend bool operator==<T>( IDType<T> const& i_lhs, IDType<T> const& i_rhs );
};

Les fonctions d'ami doivent en fait être déclarées en aval avant la définition de la classe, ce qui nécessite une déclaration en aval de la classe de modèle.

Nous définissons ensuite tous les membres pour le type de base, en nous rappelant qu'il s'agit d'une classe de modèle.

Enfin, lorsque nous voulons l’utiliser, nous le typons comme suit:

class EntityT;
typedef IDType<EntityT> EntityID;
class ModelT;
typedef IDType<ModelT> ModelID;

Les types sont maintenant entièrement séparés. Les fonctions qui prennent un EntityID génèreront une erreur de compilation si vous essayez de leur fournir un ModelID, par exemple. En plus de devoir déclarer les types de base en tant que modèles, avec les problèmes que cela entraîne, il est également assez compact.

J'espérais que quelqu'un avait des commentaires ou des critiques sur cette idée?

Un problème qui m'est venu à l'esprit en écrivant ceci, dans le cas des positions et des vitesses, par exemple, est que je ne peux pas convertir entre types aussi librement qu'avant. Où, avant de multiplier un vecteur par un scalaire, je devrais faire un autre vecteur:

typedef float Time;
typedef Vector3 Position;
typedef Vector3 Velocity;

Time t = 1.0f;
Position p = { 0.0f };
Velocity v = { 1.0f, 0.0f, 0.0f };

Position newP = p + v*t;

Avec mon typedef fortement typé, je devrais dire au compilateur que multiplier une Velocity par Time donne une Position.

class TimeT;
typedef Float<TimeT> Time;
class PositionT;
typedef Vector3<PositionT> Position;
class VelocityT;
typedef Vector3<VelocityT> Velocity;

Time t = 1.0f;
Position p = { 0.0f };
Velocity v = { 1.0f, 0.0f, 0.0f };

Position newP = p + v*t; // Compiler error

Pour résoudre ce problème, je pense que je devrais spécialiser chaque conversion de manière explicite, ce qui peut être un peu gênant. D'autre part, cette limitation peut aider à prévenir d'autres types d'erreurs (par exemple, en multipliant une vitesse par une distance, ce qui n'aurait peut-être pas de sens dans ce domaine). Je suis donc déchiré et je me demande si les gens ont des opinions sur mon problème initial ou sur mon approche pour le résoudre.

Kian
la source
Jetez un oeil à ceci: zumalifeguard.wikia.com/wiki/Idtypes.idl
zumalifeguard
la même question est ici: stackoverflow.com/q/23726038/476681
BЈовић

Réponses:

40

Ce sont des paramètres de type fantôme , c’est-à-dire des paramètres de type paramétré utilisés non pas pour leur représentation, mais pour séparer différents «espaces» de types avec la même représentation.

Et en parlant d’espaces, c’est une application utile des types fantômes:

template<typename Space>
struct Point { double x, y; };

struct WorldSpace;
struct ScreenSpace;

// Conversions between coordinate spaces are explicit.
Point<ScreenSpace> project(Point<WorldSpace> p, const Camera& c) {  }

Comme vous l'avez vu, les types d'unités posent quelques problèmes. Une chose que vous pouvez faire est de décomposer les unités en un vecteur d’exposants entiers sur les composants fondamentaux:

template<typename T, int Meters, int Seconds>
struct Unit {
  Unit(const T& value) : value(value) {}
  T value;
};

template<typename T, int MA, int MB, int SA, int SB>
Unit<T, MA - MB, SA - SB>
operator/(const Unit<T, MA, SA>& a, const Unit<T, MB, SB>& b) {
  return a.value / b.value;
}

Unit<double, 0, 0> one(1);
Unit<double, 1, 0> one_meter(1);
Unit<double, 0, 1> one_second(1);

// Unit<double, 1, -1>
auto one_meter_per_second = one_meter / one_second;

Ici, nous utilisons des valeurs fantômes pour baliser les valeurs d’exécution avec des informations au moment de la compilation sur les exposants des unités impliquées. Cela va mieux que de créer des structures séparées pour les vitesses, les distances, etc., et pourrait suffire à couvrir votre cas d'utilisation.

Jon Purdy
la source
2
Hum, utiliser le système de gabarit pour imposer des unités aux opérations est cool. Je n'y avais pas pensé, merci! Maintenant, je me demande si vous pouvez appliquer des mesures telles que les conversions entre mètre et kilomètre, par exemple.
Kian
@Kian: Vous utiliseriez probablement les unités de base du système interne (m, kg, s, A, etc.) et définiriez simplement un alias 1 km = 1 000 m pour plus de commodité.
Jon Purdy
7

Dans un cas similaire, je souhaitais distinguer différentes significations de certaines valeurs entières et interdire les conversions implicites entre elles. J'ai écrit un cours générique comme ceci:

template <typename T, typename Meaning>
struct Explicit
{
  //! Default constructor does not initialize the value.
  Explicit()
  { }

  //! Construction from a fundamental value.
  Explicit(T value)
    : value(value)
  { }

  //! Implicit conversion back to the fundamental data type.
  inline operator T () const { return value; }

  //! The actual fundamental value.
  T value;
};

Bien sûr, si vous voulez être encore plus en sécurité, vous pouvez également choisir le Tconstructeur explicit. Le Meaningest ensuite utilisé comme ceci:

typedef Explicit<int, struct EntityIDTag> EntityID;
typedef Explicit<int, struct ModelIDTag> ModelID;
Mindriot
la source
1
C'est intéressant, mais je ne suis pas sûr que ce soit assez fort. Cela garantira que si je déclare une fonction avec le type typedefed, seuls les bons éléments peuvent être utilisés comme paramètres, ce qui est bien. Mais pour chaque autre utilisation, il ajoute une surcharge syntaxique sans empêcher le mélange de paramètres. Dites des opérations telles que la comparaison. L'opérateur == (int, int) prendra un EntityID et un ModelID sans se plaindre (même si explicit nécessite que je le lance, cela ne m'empêche pas d'utiliser les mauvaises variables).
Kian
Oui. Dans mon cas, je devais m'empêcher d'attribuer différentes sortes d'identifiants les uns aux autres. Les comparaisons et les opérations arithmétiques n'étaient pas ma préoccupation principale. La construction ci-dessus interdira l'affectation, mais pas les autres opérations.
mindriot
Si vous êtes prêt à consacrer plus d’énergie à cela, vous pouvez créer une version (assez) générique qui gère également les opérateurs, en faisant en sorte que la classe Explicit englobe les opérateurs les plus courants. Voir pastebin.com/FQDuAXdu pour un exemple - vous avez besoin des constructions SFINAE assez complexes pour déterminer si la classe d'emballage fournit effectivement les opérateurs enveloppés ou non (voir cette question SO ). Remarquez, cela ne couvre toujours pas tous les cas et peut ne pas en valoir la peine.
mindriot le
Bien que sa syntaxe soit élégante, cette solution entraînera une perte de performances significative pour les types entiers. Les entiers peuvent être passés via des registres, les structures (même contenant un seul entier) ne peuvent pas.
Ghostrider
1

Je ne suis pas sûr de savoir comment cela fonctionne dans le code de production (je suis un débutant en programmation C ++, comme, par exemple, le débutant en CS101), mais je l’ai concocté à l’aide du système de macros du C ++.

#define newtype(type_, type_alias) struct type_alias { \

/* make a new struct type with one value field
of a specified type (could be another struct with appropriate `=` operator*/

    type_ inner_public_field_thing; \  // the masked_value
    \
    explicit type_alias( type_ new_value ) { \  // the casting through a constructor
    // not sure how this'll work when casting non-const values
    // (like `type_alias(variable)` as opposed to `type_alias(bare_value)`
        inner_public_field_thing = new_value; } }
Noein
la source
Remarque: s'il vous plaît laissez-moi savoir de tous les pièges / améliorations que vous pensez.
Noein
1
Pouvez-vous ajouter du code montrant comment cette macro est utilisée - comme sur les exemples de la question d'origine? Si c'est le cas, c'est une excellente réponse.
Jay Elston