web-dev-qa-db-fra.com

Typedef typé C ++

J'ai essayé de penser à un moyen de déclarer des typedefs fortement typés, pour attraper une certaine classe de bogues au stade de la compilation. C'est souvent le cas que je saisis un int dans plusieurs types d'id, ou un vecteur à positionner ou à 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 vitesse peut-être.

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 typedefs fortement typés incluent l'utilisation de boost, ce qui n'est pas possible pour moi (au moins, j'ai c ++ 11 au moins). Donc, après un peu de réflexion, je suis tombé sur cette idée, et je voulais l'exécuter par quelqu'un.

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

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 Friend doivent en fait être déclarées avant la définition de classe, ce qui nécessite une déclaration avant de la classe modèle.

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

Enfin, lorsque nous voulons l'utiliser, nous l'avons typé comme:

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

Les types sont désormais entièrement séparés. Les fonctions qui prennent un EntityID génèrent une erreur de compilation si vous essayez de leur fournir un ModelID à la place, par exemple. En plus d'avoir à déclarer les types de base en tant que modèles, avec les problèmes que cela implique, 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, serait que je ne peux pas convertir entre les types aussi librement qu'auparavant. Où avant de multiplier un vecteur par un scalaire donnerait un autre vecteur, donc je pourrais faire:

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 le multi-échantillonnage d'une vitesse par un temps entraîne 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 explicitement chaque conversion, ce qui peut être un peu gênant. D'un autre côté, cette limitation peut aider à éviter d'autres types d'erreurs (par exemple, multiplier une vitesse par une distance, peut-être, ce qui n'aurait 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 d'origine ou sur mon approche pour le résoudre.

52
Kian

Ce sont paramètres de type fantôme, c'est-à-dire des paramètres d'un type paramétré qui ne sont pas utilisés 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, cependant, il existe certaines difficultés avec les types d’unités. 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 valeurs fantômes pour marquer les valeurs d'exécution avec des informations au moment de la compilation sur les exposants sur les unités impliquées. Cela évolue mieux que la création de structures distinctes pour les vitesses, les distances, etc., et peut être suffisant pour couvrir votre cas d'utilisation.

41
Jon Purdy

J'ai eu un cas similaire où je voulais distinguer différentes significations de certaines valeurs entières et interdire les conversions implicites entre elles. J'ai écrit une classe générique comme celle-ci:

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 sûr, vous pouvez également faire le constructeur Texplicit. Le Meaning est alors utilisé comme ceci:

typedef Explicit<int, struct EntityIDTag> EntityID;
typedef Explicit<int, struct ModelIDTag> ModelID;
7
mindriot

Je ne sais pas comment cela fonctionne dans le code de production (je suis un débutant en C++/programmation, comme le débutant CS101), mais je l'ai préparé en utilisant le macro sys de 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; } }
1
Noein