Contenu | Rechercher | Menus

Annonce

Si vous avez des soucis pour rester connecté, déconnectez-vous puis reconnectez-vous depuis ce lien en cochant la case
Me connecter automatiquement lors de mes prochaines visites.

À propos de l'équipe du forum.

#1 Le 07/12/2017, à 00:24

Destroyers

[C++...+] Méta-programmation arithmétique avec des flottants [RESOLU]

Bonjour à toi qui passe par là.
Je suis novice en métaprogrammation, quoique j'ai de bonnes bases avec les types_traits. Néanmoins je requière de l'aide.


Version courte : J'ai besoin de connaître le modulo de deux doubles au compile-time. Comment faire ?

version longue : Je code une librairie de gestion d'unités physiques inspirée de std::chrono::duration. Cependant, j'ai des difficultés à représenter certaines sous-unités avec std::ratio. Par exemple std::ratio<1, 1> dans une classe Mass va représenter le Kilogramme, et les valeurs qui peuvent être prises par std::ratio (des intmax_t, limitées à 10^18) m’empêchent de représenter l'unité de masse atomique (qui nécessite un ratio avec un dénominateur valant   ~ 10^35).

Donc j'ai décidé de coder ma propre classe ratio prenant des doubles, ce type pouvant aller jusqu'à 10^310.

template<double const& _Num, double const& _Den = 1>
class ratio{};

Or, je veux tout de même que_ Num et _Den soient des entiers. Je place donc un static_assert qui teste cette condition.
Pour se faire, il suffit de tester si :     _Num % 1 = 0
Or, vous savez comme moi que l'opérateur % ne fonctionne pas avec des flottants. std::remainder ou std::modulus ne sont pas des solutions car ce sont des fonctions appelées au run-tume, or je cherche a optimiser un maximum...

La bonne solution pour moi était celle-ci :

template <double const& X, double const& Y, bool>
class quotient_impl;


template <double const& X, double const& Y>
class quotient_impl<X, Y, false>
{
  static constexpr double newY = X - Y;
public:
  static constexpr double value = quotient_impl<X, newY, X < Y >::value;
};


template <double const& X, double const& Y>
class quotient_impl<X, Y, true>
{
public:
  static constexpr double value = Y;
};


template <double const& X, double const& Y>
class quotient
{
public:
  static constexpr double value = quotient_impl<X, Y, X < Y >::value;
};


template <double const& X, double const& Y>
class modulo
{
public:
  static constexpr double value = X - (Y * quotient<X, Y>::value);
};

Voici le main que je fournis pour le test :

#include <iostream>

static constexpr double num = 10;
static constexpr double den = 3;

int main()
{
    //je test quotient et non molulo, donc c'est pas modulo qui pose problème
    std::cout << quotient<num, den>::value <<'\n';
    return 0;
}

Et l'erreur est sans appel :

fatal error: template instantiation depth exceeds maximum of 900
 static constexpr double newY = X - Y;
                         ^~~~

C'est comme si la condition X<Y n'était jamais vraie, Alors qu'elle devrait bien le devenir au bout de 3 "itérations" (récursions).
Merci de m'éclairer smile

PS : newY est nécessaire. En effet je ne peux pas faire directement quotient_impl<X, X - Y, X < Y > car X - Y est une rvalue et un template double non-type ne peux prendre que des expressions constantes (donc un static constexpr double... soit une lvalue).

Dernière modification par Destroyers (Le 10/01/2018, à 12:26)

Hors ligne

#2 Le 07/12/2017, à 10:28

Destroyers

Re : [C++...+] Méta-programmation arithmétique avec des flottants [RESOLU]

Très bien, j'étais simplement dans les choux, j'avais simplement mal fait les calculs, désolé d'avoir posté un topic inutile.

Malgré tout, voici la solution pour ceux que ça intéresse :

template <double const& X, double const& Y, bool>
class modulo_impl;


template <double const& X, double const& Y>
class modulo_impl<X, Y, false>
{
  static constexpr double newX = X < Y ? X : X - Y;
public:
  static constexpr double value = modulo_impl<newX, Y, X < Y >::value;
};


template <double const& X, double const& Y>
class modulo_impl<X, Y, true>
{
public:
  static constexpr double value = X;
};


template <double const& X, double const& Y>
class modulo
{
public:
  static constexpr double value = modulo_impl<X, Y, X < Y >::value;
};

Ce n'est pas Y qu'il faut modifier à chaque récursion, c'est X. En plus de fonctionner, cela donne directement le modulo, donc plus besoin de passer par le calcul du quotient.

Je me pose néanmoins toujours une petite question, est ce que la condition :

static constexpr double newX = X < Y ? X : X - Y;

se résout bien au compile-time ?

Voila, si vous avez malgré tout des suggestions ou des optimisations à apporter, je suis preneur smile





EDIT : Cette manière de faire ne fonctionne que si X et Y sont positifs. Vu que je n'ai pas besoin de gérer les nombres négatifs vu l'utilisation que je veux en faire, j'ai ajouté des static_assert

template <double const& X, double const& Y>
class modulo
{
public:
  static_assert(X >= 0 && Y >= 0 , "modulo doesn't manage negative numbers.");
  static_assert(Y > 0 , "denominator cannot be zero.");

  static constexpr double value = modulo_impl<X, Y, X < Y >::value;
};

EDIT 2 : Si le numérateur du modulo est 900 fois plus grand que le dénominateur, il y a trop de récursion à réaliser et le compilateur provoque une erreur (depth exeeds maximum of 900). J'utilise alors une option de compilation me permettant d'augmenter cette limite, mais même avec cette option on ne peut pas monter à plus de 65535 récursions... Or j'ai besoin de 10^35 récursions... Je pense donc qu'il faut que je lâche la métaprogrammation par template et que je teste la métaprogrammation par expression constante... cela dit, j'ai peur que tout ne se fasse pas au compile-time avec cette seconde méthode.


EDIT 3 : En fait, la métaprogrammation à base d’expression constante a le même souci, il y a la même limitation de récursion qu'avec les templates. Sauf que cela ne se voit pas, cela ne provoque pas d'erreur de compilation car si le compilateur ne peut pas résoudre la récursion, alors il considère la fonction constexpr comme une simple fonction normale qui sera évaluée au runtime. Enfin bref, je pensais que la métaprogrammation était quelque chose de fabuleux, mais elle possède tout de même ses limites (de grosses limites) : elle ne fonctionne que pour des "petits" nombres et n'est pas très adaptée pour les calculs de flottants...


EDIT 4 : Finalement j'ai utilisé une fonction constexpr (expression constante) dans laquelle il est possible de faire une boucle classique (SANS récursion) depuis le c++14. Une fonction constexpr est évaluée au compile-time si les arguments passés en paramètres sont constexpr (ce qui est le cas avec moi) et si les fonctions qui sont utilisées dedans sont constexpr. Heureusement, std::floor() est constexpr. Voici donc ma fonction modulo évaluée au compile-time :

template <typename T, typename U>
constexpr typename std::common_type<
typename std::enable_if<std::is_arithmetic<T>::value, T>::type,
typename std::enable_if<std::is_arithmetic<U>::value, U>::type>::type
modulo(T const& a, U const& b)
{
  typedef typename std::common_type<T, U>::type common;
  common a2 = static_cast<common>(a);
  common b2 = static_cast<common>(b);
  return a2 - (std::floor(a2/b2) * b2);
}

Le type de retour est donc le type commun entre T et U (grâce à std::commont_type) et la fonction ne se crée que si T et U sont des types arithmétiques (entiers et flottants) (grâce à std::is_arithmetic).
Cette méthode a néanmoins l’inconvénient de nécessiter un compilateur très récent (sur gcc ça ne compile pas sur les versions antérieures à 6.2.0 et sur visual-studio, il faut au minimum VS2017) mais bon, c'est le prix d'un code moderne.

j'espère en avoir aidé plus d'un smile

Dernière modification par Destroyers (Le 10/01/2018, à 12:31)

Hors ligne