Surcharge d'opérateurs et converstion implicite

Le problème exposé dans ce sujet a été résolu.

Bonjour,

Je m’amuse à implémenter une petite classe Fraction en C++17. Pour le moment j’ai fait toutes les opérations possibles entre deux instances de cette classe. Sans trop faire attention j’ai rentré ça pour essayer mon implémentation :

std::cout << f * Fraction{-3, 7} + 5 << std::endl;

Or je n’ai jamais défini l’opération d’addition entre une fraction et un entier dans ma classe. Mais magie… Ça compile et en plus ça affiche le bon résultat ! o_O

Je suppose que la valeur 5 est implicitement convertie en une instance de Fraction, non ? Voici comment est défini le constructeur :

Fraction(int const numerator = 0, int const denominator = 1);

Mes questions sont :

  • Est-ce bien une conversion implicite ?
  • Devrais-je définir l’opération entre un entier et une fraction si ça fonctionne très bien comme ça ? Ça permettrai d’éviter d’instancier un objet Fraction.
  • Si j’implémente cette opération, comment être sûr que le compilateur utilisera bien celle-ci et non une conversion implicite ?
  • Dois-je également définir les opérations habituelles pour les short, unsigned… ? Ou si elles sont définies pour les int c’est bon ? Quelles sont les bonnes pratiques à ce niveau là ?

Merci pour vos réponses !

+0 -0
  • Oui
  • Ca va dépendre de la symétrie voulue. Se reposer sur la conversion implicite permet d’avoir une symétrie a moindre coût (ie int + frac, frac + int). En général, j’ai tendance a préciser les opérations avec des types différents que si l’operation demande un ordre des arguments bien precis (ie nombre x vecteur a du sens mais pas vecteur x nombre).
  • Je pense que les conversions implicites passent apres un match exact mais sans garantie. Les réglés sont la (énorme pave imbitable C++ style).
  • J’aurais tendance a dire non car tout va se convertir en int. Et ca peut generer des ambiguites dans certains cas. Ou alors utiliser des template
+0 -0
  • C’est effectivement une conversion implicite qui a lieux.
  • Si ta classe Fraction est bien défini, il y a de fortes chances pour que le code compilé soit exactement le même que si tu n’avais pas de création d’objets temporaires. Si tu fais des trucs dans ton constructeur (genre allocation, afficher du debug ou autre), le compilateur ne pourra par supprimer ce genre d’opérations.
  • Quand le compilateur a le choix entre plusieurs options, il y a des règles précises (mais très compliqués) pour décider quel option choisir. Dans ton cas, le compilateur aura le choix entre appeler ton opérateur avec un type qui n’est pas le bon (nécessite un appel à un constructeur implicite) et appeler ton opérateur avec un type qui est le bon. Ça me parait que le compilateur ne prendra pas l’option de l’appel à un constructeur implicite.
  • La conversion implicite de short à int ne fera pas moins ni plus que ce que tu feras en définissant ces opérations explicitement. Je ne pense pas que ce soit utile de le faire.

Un petit détail par rapport aux bonnes pratiques: il faut faire très attention aux conversions implicites pour éviter des bugs souvent difficile à comprendre. De manière générale, si le type d’origine et de ta classe sont sémantiquement similaires (c’est le cas ici) ça pose rarement des problèmes. En revanche, si tu créés une classe vecteur qui (au hasard) peut être construite avec un entier pour indiquer le nombre d’éléments, tu risques d’avoir des problèmes.

Un autre point important pour l’utilisation de ta classe: ta fraction est défini avec un numérateur et un dénominateur. Cependant, rien ne force ceux-ci à être des int. Il serait intéressant (mais légèrement plus compliqué) de faire en sorte que ce type soit défini via un template pour laisser un potentiel utilisateur à choisir le type qui conviens (par exemple un entier en précision arbitraire ou un entier sur 64 bits).

Merci pour vos réponses. Ça m’a permis de simplifier et d’éclaircir mon code comme il faut.

Un autre point important pour l’utilisation de ta classe: ta fraction est défini avec un numérateur et un dénominateur. Cependant, rien ne force ceux-ci à être des int. Il serait intéressant (mais légèrement plus compliqué) de faire en sorte que ce type soit défini via un template pour laisser un potentiel utilisateur à choisir le type qui conviens (par exemple un entier en précision arbitraire ou un entier sur 64 bits).

Je suis d’accord avec ça. C’est en fait ce que je comptais faire une fois la classe testée avec des int (sûrement une perte de temps mais comme je ne suis pas pressé ;) ). La seule chose qui me dérange c’est que si j’utilise des templates, je suis obligé de mettre tout le code de ma classe dans le header, c’est bien ça ?

La seule chose qui me dérange c’est que si j’utilise des templates, je suis obligé de mettre tout le code de ma classe dans le header, c’est bien ça ?

Wizix

Une pratique assez courante est de mettre les déclarations dans un fichier header (fraction.hpp) et les définitions dans un fichiers séparé (fraction.inl). Ce dernier est ensuite inclus à la fin du fichier header.

Merci pour cette réponse.

J’obtiens donc cette classe :

template<typename T>
class Fraction
{
public:
  Fraction(T const &numerator, T const &denominator)
  {
      set_fraction(numerator, denominator);
  }

  // Autres méthodes
}

J’avais avant l’opérateur + défini de cette façon :

inline Fraction operator+(Fraction lhs, Fraction const &rhs)
{
   lhs += rhs;
   return lhs;
}

Comment dois-je le définir maintenant ?

template<typename A, typename B, typename C>
inline Fraction<C> operator+(Fraction<A> lhs, Fraction<B> const &rhs)
{
   lhs += rhs;
   return lhs;
}

C’est un peu bizarre non ? Je trouve ça bizarre sûrement du à mon manque d’expérience :-°

EDIT : désolé j’ai trouvé la réponse à mon problème, question pas très maligne je pense.

Vous pouvez trouver ma classe sur ce Pastebin. Le problème que j’ai maintenant est que depuis que j’utilise les templates :

Fraction f{24, -2};
f += 5; // Pas de soucis

std::cout << Fraction{-5, 2} + 5 << std::endl; // Ne compile pas, Invalid operands to binary expression ('Fraction<int>' and 'int')

Comment récupérer les avantages de la conversion implicite ?
Merci

+0 -0

C’est un problème de surcharge.

Pour faire simple, quand tu crées ton objet Fraction f{24, -2}, il est de type Fraction<int> et une fonction membre Fraction & operator+=(Fraction<int>) est immédiatement instanciée (même si elle n’est pas appelée). Du coup, quand tu appelles cet opérateur par la suite, il est déjà instancié et ton entier peut être converti en Fraction<int> sans problème.

En revanche, ta fonction Fraction<T> operator+(Fraction<T>, Fraction<T>) est une fonction libre et elle ne sera instanciée que si les arguments correspondent aux paramètres. Comme les arguments sont Fraction<int> et int, la fonction est donc écartée. Comme elle n’est pas instanciée, le compilateur ne peut même pas essayer de convertir les arguments.

Une solution : transformer ces fonctions libres en fonctions membres amies de classe (avec le mot-clé friend). De cette manière, elles seront instanciées en même temps que la classe.

Petit détail : ça serait encore mieux de n’accepter que les nombres entiers comme type de la classe template. Ce qu’on peut réaliser avec une ligne verbeuse :

template<typename T, typename = std::enable_if_t<std::is_integral_v<T>>>

ou avec quelque chose de ce style si on attend la prochaine norme :

template<std::Integral T>
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte