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?
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.
Il y a deux choses différentes impliquées dans votre code:
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).
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*:
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
).hours
en seconds
sans avoir à multiplier manuellement par 3600.auto speed = 70km / 1h; // Don't bother deducing the type of speed, let the compiler do it for you.
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:
int
, double
, ...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++:
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 (*
, /
, ...).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};
}