web-dev-qa-db-fra.com

Unités pour les types en C ++

Dans les C++ Core Guidlines P.1 change_speed exemple, il montre un type Speed utilisé comme indiqué ci-dessous:

change_speed(Speed s); // better: the meaning of s is specified
// ...
change_speed(2.3); // error: no unit
change_speed(23m / 10s); // meters per second

Je suis particulièrement intéressé par les deux dernières lignes de cet exemple. Le premier semble suggérer que si vous ne fournissez aucune unité avec l'argument à change_speed cela générera une erreur. La dernière ligne affiche les unités définies à l'aide des littéraux m et s. Ces deux nouvelles fonctionnalités sont-elles présentes dans les versions modernes de C++? Si tel est le cas, comment quelque chose comme cela serait-il implémenté et quelle version de C++ est requise?

18
rozzy

Comme mentionné dans les commentaires, l'exemple des directives de base utilise des littéraux définis par l'utilisateur pour construire des types spécifiques à l'application qui représentent intuitivement des quantités physiques. Pour les illustrer pour l'exemple spécifique, considérez ces types:

/* "Strong" speed type, unit is always [m/s]. */
struct Speed {
   long double value;
};

/* "Strong" length type, parameterized by a unit as multiples of [m]. */    
template <class Period = std::ratio<1>> struct Length {
   unsigned long long value;
};

Il n'est probablement pas très logique de suivre l'unité des objets Length, mais pas pour les instances Speed, mais considérons l'exemple le plus simple possible ici. Maintenant, regardons deux littéraux définis par l'utilisateur:

#include <ratio>

auto operator ""_m(unsigned long long n)
{
   return Length<>{n};
}

auto operator ""_km(unsigned long long n)
{
   return Length<std::kilo>{n};
}

Ils vous permettent d'instancier Length objets comme ceci:

/* We use auto here, because the suffix is so crystal clear: */
const auto lengthInMeter = 23_m;
const auto lengthInKilometer = 23_km;

Afin de cosntruire une instance Speed, définissons un opérateur approprié pour diviser un Length par un duration:

#include <chrono>

template <class LengthRatio, class Rep, class DurationRatio>
auto operator / (const Length<LengthRatio>& lhs,
      const std::chrono::duration<Rep, DurationRatio>& rhs)
{
   const auto lengthFactor = static_cast<double>(LengthRatio::num)/LengthRatio::den;
   const auto rhsInSeconds = std::chrono::duration_cast<std::chrono::seconds>(rhs);

   return Speed{lengthFactor*lhs.value/rhsInSeconds.count()};
}

Maintenant, regardons à nouveau l'exemple des directives de base,

void change_speed(const Speed& s)
{
    /* Complicated stuff... */
}

mais surtout, comment appeler une telle fonction:

using namespace std::chrono_literals;

int main(int, char **)
{
   change_speed(23_m/1s);
   change_speed(42_km/3600s);
   change_speed(42_km/1h);

   return 0;
}

Comme @KillzoneKid l'a mentionné dans les commentaires, C++ 11 est requis pour que cela fonctionne.

17
lubgr

Il y a deux choses différentes impliquées dans votre code:

  1. L'utilisation de types forts/unitaires pour rendre votre code plus robuste, c'est-à-dire que vous différenciez deux types entiers. Ceci est intégré dans certains langages (par exemple, Ada), mais pas en C++, mais vous pouvez créer des classes qui encapsulent des types entiers pour imiter un tel comportement (voir ci-dessous).

  2. L'utilisation de littéraux d'opérateur pour créer des instances de ces classes de manière conviviale, c'est-à-dire que vous écrivez 1s au lieu de seconds{1}. Il s'agit simplement d'une fonction pratique, qui peut être utile à certains endroits.

L'utilisation de types entiers forts est très utile car elle rend votre code beaucoup moins sujet aux erreurs*:

  • Vous ne pouvez pas convertir entre des types représentant des durées et des longueurs, comme dans la vie réelle.
  • Dans les types représentant le même genre de choses (par exemple, seconds et hours), il n'y a pas de conversions implicites si vous perdez la précision, par exemple, vous ne pouvez pas convertir seconds en hours, sauf si vous représentez our avec un type à virgule flottante (float/double).
  • Les conversions (implicites et non implicites) s'occupent de la mise à l'échelle pour vous: vous pouvez convertir hours en seconds sans avoir à multiplier manuellement par 3600.
  • Vous pouvez fournir des opérateurs réalistes qui prendront en charge les conversions pour vous, par exemple, dans votre exemple, il y a un opérateur de division entre la longueur et la durée qui donne des vitesses. Le type exact de vitesse est automatiquement déduit du type de la longueur et du type de durée:
auto speed = 70km / 1h; // Don't bother deducing the type of speed, let the compiler do it for you.
  • Les types d'unités sont auto-documentés : si une fonction renvoie microseconds, vous savez ce que c'est, vous n'avez pas à espérer que le type documentant la fonction renvoyant unsigned long long a mentionné que cela représente des microsecondes ...

* Je ne parle que de conversion implicite ici, bien sûr, vous pouvez faire une conversion explicitement, par exemple, en utilisant duration_cast (perte de précision).


Types d'unités

L'encapsulation de types entiers dans des classes "unit" a toujours été disponible, mais C++ 11 a apporté un type entier encapsulé standard: std::chrono::duration .

Une classe "unité" peut être définie par:

  • le type de chose qu'il représente: temps, longueur, poids, vitesse, ...
  • le type C++ qu'il utilise pour représenter ces données: int, double, ...
  • le rapport entre ce type et le "type 1" de la même unité.

Actuellement, seuls les types de durée sont fournis par la norme, mais il y a eu des discussions (peut-être une proposition?) Pour fournir un type d'unité de base plus générique tel que:

template <class Unit, class Rep, class Ratio = std::ratio<1>> class unit;

... où Unit serait un espace réservé indiquant le type de chose représentée, par exemple:

struct length_t { };

template <class Rep, class Ratio = std::ratio<1>>
using length = unit<length_t, Rep, Ratio>;

Mais ce n'est pas encore standard, alors regardons std::chrono::duration:

template <class Rep, class Period = std::ratio<1>> class duration;

Les paramètres du modèle Rep sont de type C++:

  • Les types de durées définies standard ont des représentations entières (définies par l'implémentation).
  • Le type sous-jacent définit le type de conversions qui peuvent être implicitement effectuées:
    • Vous pouvez convertir les heures entières en secondes entières implicitement (multiplier par 3600).
    • Vous pouvez convertir les secondes entières en heures entières implicitement car vous perdriez la précision.
    • Vous pouvez convertir des secondes entières en double heures.

Les paramètres du modèle Period définissent le rapport entre le type duration et une seconde (qui est la durée de base choisie):

  • std::ratio est un type défini par la norme très pratique qui représente simplement un rapport entre deux entiers, avec les opérations correspondantes (*, /, ...).
  • Le standad propose plusieurs types de durée avec différents ratios: std::chrono::seconds, std::chrono::minutes, ...

Littéraux d'opérateur

Ceux-ci ont été introduits en C++ 11 et sont opérateurs littéraux .

Le s est standard et est inclus dans la bibliothèque standard chrono :

using namespace std::chrono_literals;
auto one_second = 1s;
auto one_hour = 1h;

Le m n'est pas standard, et doit donc être préfixé par _ (car il est défini par l'utilisateur), comme 23_m. Vous pouvez définir votre propre opérateur comme suit:

constexpr auto operator "" _m(unsigned long long ull) { 
    return meters{ull};
}
3
Holt