IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Langage C++ Discussion :

Conversions implicites et opérateurs arithmétiques


Sujet :

Langage C++

  1. #1
    Modérateur

    Avatar de Bktero
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Juin 2009
    Messages
    4 481
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : France, Loire Atlantique (Pays de la Loire)

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Juin 2009
    Messages : 4 481
    Points : 13 679
    Points
    13 679
    Billets dans le blog
    1
    Par défaut Conversions implicites et opérateurs arithmétiques
    Bonjour,

    Je code une classe représentant de nombres à virgule fixe. Du coup, j'ai rajouté des opérateurs arithmétiques ainsi que des constructeurs et opérateurs de conversion implicites.

    Voici un code représentatif de ma classe telle qu'elle est actuellement (d'un point de vue API, l'implem est bidon) :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    template<typename T, std::uint8_t I = 7, std::uint8_t F = 24>
    class Number {
     
    public:
    	Number() = default;
     
    	template<typename U>
    	Number(U value) :
    			raw_data(value) {
    	}
     
    	Number(float value) :
    			raw_data(value) {
    	}
     
    	operator float() const {
    		return raw_data;
    	}
     
    	Number operator+(const Number& other) {
    		Number result;
    		result.raw_data = raw_data + other.raw_data;
    		return result;
    	}
     
    private:
    	T raw_data;
    };
    Je peux écrire du code comme ça, c'est cool :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    	using Number = Number<std::uint32_t>;
     
    	Number f = 2;
    	Number g(3.14f);
    	Number h = f + g;
    	float j = h;
    Par contre, je ne peux pas écrire ceci :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    	Number f = 2;
    	Number i = f + 12;
    Je sais que pour réaliser ce genre d'opérations "mixed-mode", il faut faire des opérateurs amis. J'ai donc modifié mon opérateur + en ceci :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    	friend Number operator+(const Number& lhs, const Number& rhs) {
    		Number result;
    		result.raw_data = lhs.raw_data + rhs.raw_data;
    		return result;
    	}
    Mais ce n'est pas suffisant, mon opérateur de conversion non 'explicit' fout visiblement la zouille :
    error: ambiguous overload for 'operator+' (operand types are 'Number {aka Number<unsigned int>}' and 'int')
      Number i = h + 12;
                 ~~^~~~
    note: candidate: operator+(float, int) <built-in>
    note: candidate: Number<unsigned int> operator+(const Number<unsigned int>&, const Number<unsigned int>&)
    En le marquant comme 'explicit', le problème disparaît mais une erreur autre apparaît quand je fais float value = i;, naturellement... En faisait float value = static_cast<float>(i);, tout compile.

    Le problème est que j'ai passé l'opérateur + en tant qu'ami pour éviter de faire de convertir explicitement lors d'une addition (par exemple Number i = h + Number(12);) mais au final je me retrouve à avoir déplacer le casting. Je pense que la nouvelle situation est meilleure, il y aura moins de conversions explicites à faire. Mais bon le lait, le beurre, l'argent, la crémière tout ça, j'aimerais bien ne plus avoir de cast du tout

    Une solution pour moi ?

    Merci d'avance !

  2. #2
    Modérateur

    Avatar de Bktero
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Juin 2009
    Messages
    4 481
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : France, Loire Atlantique (Pays de la Loire)

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Juin 2009
    Messages : 4 481
    Points : 13 679
    Points
    13 679
    Billets dans le blog
    1
    Par défaut
    En faisant des tests supplémentaires, je me rends compte que le problème n'est pas vraiment le fait que mon opérateur + ne soit pas ami, c'est plutôt que l'opérateur de conversion est implicite. En rajoutant 'explicit' et en laissant + comme étant membre, je peux écrire ceci sans problème :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    	Number f = 2;
    	Number g(3.14f);
    	Number h = f + g;
     
    	Number i = h + 12; // plus de problème :)
    Il reste la manière de reconvertir mon Number en float. Je me suis rendu compte que je pouvais écrire quelque chose comme ceci float value(i); à la manière d'une "direct initialization". Il reste des static_cast comme dans le code suivant mais ça me permet d'en mettre moins et je crois que je vais me satisfaire de cet état.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    void funct(const float value);
     
    funct(i); // erreur !
    funct(static_cast<float>(i)); // OK

  3. #3
    Modérateur

    Avatar de Bktero
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Juin 2009
    Messages
    4 481
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : France, Loire Atlantique (Pays de la Loire)

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Juin 2009
    Messages : 4 481
    Points : 13 679
    Points
    13 679
    Billets dans le blog
    1
    Par défaut
    Au temps pour moi, avec un opérateur membre, je ne peux pas écrire ceci :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    Number h;
    Number i = h + 12; // OK
    i = 12 + h; // KO
    Il faut donc bien que l'opérateur soit non membre et ami.

  4. #4
    Expert éminent sénior
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Février 2005
    Messages
    5 069
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : France, Val de Marne (Île de France)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Conseil

    Informations forums :
    Inscription : Février 2005
    Messages : 5 069
    Points : 12 113
    Points
    12 113
    Par défaut
    Ça serait pas plus élégant avec des "user literal" ?
    http://en.cppreference.com/w/cpp/language/user_literal

  5. #5
    Membre émérite
    Avatar de prgasp77
    Homme Profil pro
    Ingénieur en systèmes embarqués
    Inscrit en
    Juin 2004
    Messages
    1 306
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : France, Eure (Haute Normandie)

    Informations professionnelles :
    Activité : Ingénieur en systèmes embarqués
    Secteur : High Tech - Électronique et micro-électronique

    Informations forums :
    Inscription : Juin 2004
    Messages : 1 306
    Points : 2 466
    Points
    2 466
    Par défaut
    Bonjour,

    permettez-moi de vous donner mon avis sur une façon que je considère élégante de résoudre votre problème. Mais avant tout, d'où vient le problème ?

    L'ambiguïté
    Lorsque l'on écrit Number<int> g = f + 12;, l'implémentation considère les candidats possibles. Les règles pour déterminer les candidats sont notamment listés dans [basic.lookup.unqual]. Dans ce cas, une simplification possible est de dire que sont considérés toutes les chaînes de conversions ne convertissant chaque identifiant qu'une fois au maximum. On remarque donc qu'il en existe plusieurs :
    1. f -> float (1.1) => operator+(float, int) (builtin) => float -> Number<int> (1.2)
    2. 12 -> Number<int> (2.1) => Number<int>::operator+(Number<int> const&)

    Il y a donc bien deux candidats possibles, aucun des deux n'étant préférable. Cela conduit à une ambiguïté.


    Vers une solution
    La solution que vous avez trouvée est de casser la conversion (1.1) en la qualifiant d'explicit. Et ça fonctionne, au prix de ne plus pouvoir convertir un Number en float. Acceptable, mais perfectible.
    La solution que j'aimerais vous proposer est d'ajouter un maillon dans la conversion (1.2), en qualifiant d'explicit le constructeur template<class U> Number(U value). Mais cela n'est pas suffisant car coupe aussi la conversion (2.1). Il nous faut donc la reconstruire, et cela en passant par un type intermédiaire.

    Ce type, appelons-le ProxyNumber<T> doit avoir les caractéristiques suivantes :
    1. Être le type de l'expression f + 12
    2. Être convertible en Number<T>, et ainsi pouvoir écrire Number g = f + 12



    Ma proposition
    Une fois ces caractéristiques en tête, son écriture devient aisée. Pour une meilleur lecture, notez que j'ai simplifié votre interface de Number sans que cela ait d'impact.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    template<class T> class Number {
        T raw_data;	
        struct ProxyNumber
        {
            Number _number;
            ProxyNumber(T const& n) : _number(Number<T>{n}) {}
            operator Number() const { return _number; } // (b)
        };
    public:
        template<class U> explicit Number(U value) : raw_data(value) {}
        operator float() const { return raw_data; }
        template<class U> friend ProxyNumber operator+(Number const& lhs, U const& rhs) { const T t = lhs.raw_data + rhs; return ProxyNumber{t}; } // (a)
    };
    Ainsi, nous pouvons écrire :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    Number<int> f{2};
    Number<int> g = f + 12;
    float g_as_float = g;
    std::cout << g_as_float << "\n";
    ou même
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    Number<int> f{2};
    Number<int> g = f + 12;
    std::cout << g << "\n";
    Nous avons donc perdu la possibilité de value-initialize un Number à partir d'un int/float/..., et devons remplacer Number<int> f = 2 par Number<int> f{2};. Mais en contre partie, je vous offre, au prix de l'ajout de

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    template<class U> friend ProxyNumber operator+(U const& lhs, Number const& rhs) { const T t = rhs.raw_data + lhs; return ProxyNumber{t}; }
    la possibilité d'écrire Number<int> g = 12 + f.

    Démo sur coliru
    -- Yankel Scialom

  6. #6
    Modérateur

    Avatar de Bktero
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Juin 2009
    Messages
    4 481
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : France, Loire Atlantique (Pays de la Loire)

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Juin 2009
    Messages : 4 481
    Points : 13 679
    Points
    13 679
    Billets dans le blog
    1
    Par défaut
    Bonjour,

    Merci pour vos réponses

    1. bacelar : les user-defined litterals auraient été la bonne solution, car en fait on ne veut pas vraiment faire du mixed-mode, on veut pouvoir multiplier nos FixedPoints / Numbers par des constantes et non par des variables de types primitifs. Malheureusement, ce n'est pas possible avec les templates d'après ce que j'ai vu...

    2. prgasp77 : merci pour l'explication claire de l’ambiguïté. Je ne suis pas pleinement convaincu par votre solution en revanche. Je trouve que ça apporte une écriture bizarre :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    Number<int> f{2};
    Number<int> ff = 2; // pas possible alors que très proche de la ligne suivante en terme de style
    Number<int> g = f + 12;
    De plus, ma situation actuelle me permet de faire ça :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    	Number f = 2;
    	Number g(3.14f);
    	Number h = (17 + f) + (g + 5);
    Ce qui est la contre-partie de votre solution, que je ne gagne pas vraiment


    J'ai un peu modifié ma classe réelle, simplement en mettant mes opérateurs de conversions comme explicit et le code utilisateur a été déjà pu être largement amélioré en terme de lisibilité. Il s'avère que je n'ai même pas eu besoin de mettre en amis les opérateurs arithmétiques puisque je n'ai pas trouvé de code actuellement faisant un truc comme , il n'y a pour l'instant que des

  7. #7
    Rédacteur/Modérateur
    Avatar de JolyLoic
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2004
    Messages
    5 463
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 49
    Localisation : France, Yvelines (Île de France)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Août 2004
    Messages : 5 463
    Points : 16 213
    Points
    16 213
    Par défaut
    Quelques éléments de réflexion que je n'ai pas encore vus dans cette discussion:

    La conversion d'un T en Number<T> me semble globalement (voir point suivant) une bonne chose : Elle ne perd jamais en précision. En revanche, la conversion de Number<T> vers T me semble très dangereuse, car elle perd en précision (pas avec l'implem bidon actuelle, mais avec une vraie implem qu'on peut imaginer derrière. Est-ce à dire qu'il faut l’empêcher ? Non, juste qu'il faut s'assurer qu'elle n'ait lieu que là où l'utilisateur le veut vraiment. Donc une conversion implicite, surtout pas, même une conversion explicite me semble risquée, je partirais plutôt vers une fonction nommée (appoximate ? value ?).
    Pour donner un exemple similaire, j'ai l'occasion de travailler avec des chaînes ayant une conversion implicite en char*, et avec std::string qui a une fonction nommée c_str. Et je peux dire que dans tous les bugs ultra-compliqués à reproduire/déboguer que j'ai pu voir au cours de 5 dernières années, la fonction de conversion implicite entrait dans l'explication du problème. c_str étant beaucoup plus visible, je ne vais pas dire que je n'ai jamais vu de bug d'un pointeur retourné par un c_str qui survivait à la chaîne, mais ces bugs se localisent généralement bien plus vite.

    La conversion d'un T en Number<T> est à éviter dans la mesure du possible (oui, je sais, je me contredis, et c'est volontaire): J'imagine qu'à un moment ou à un autre, les performances vont compter, et que cette conversion est loin d'être gratuite (par exemple, il ne serait pas surprenant qu'elle demande une allocation de mémoire). Je ne dis pas qu'il faut l'interdire, je dis juste que là où elle est évitable, il faut l'éviter. Et donc fournir un operator+(T, Number<T>) en plus d'un operator+(Number<T>, T) et d'un operator+(Number<T>, Number<T>). Et avec des implémentations différentes pour ces trois opérateurs. Oui, c'est lourd... Mais c'est mieux.
    Toujours dans les comparaisons, regarde l'opérateur+ entre std::string et char*, la situation est très semblable.
    Ma session aux Microsoft TechDays 2013 : Développer en natif avec C++11.
    Celle des Microsoft TechDays 2014 : Bonnes pratiques pour apprivoiser le C++11 avec Visual C++
    Et celle des Microsoft TechDays 2015 : Visual C++ 2015 : voyage à la découverte d'un nouveau monde
    Je donne des formations au C++ en entreprise, n'hésitez pas à me contacter.

  8. #8
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 612
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 612
    Points : 30 612
    Points
    30 612
    Par défaut
    Salut,
    Citation Envoyé par JolyLoic Voir le message
    <snip>Et donc fournir un operator+(T, Number<T>) en plus d'un operator+(Number<T>, T) et d'un operator+(Number<T>, Number<T>). Et avec des implémentations différentes pour ces trois opérateurs. Oui, c'est lourd... Mais c'est mieux.
    Et encore, "lourd"... C'est à voir... Car rien n'empêche aux différentes versions de faire appel au même fonctions: entre les optimisations du compilateur et l'inlining, rien n’empêcherait d'avoir:
    • un opérateur += (Number<T> &) comme fonction membre
    • un opérateur + (Number<T> const &, Number<T> const &) qui fait appel à l'opérateur +=
    • un opérateur + (Number<T> const &, T) qui fait appel à l'opérateur + Number<T> const &, Number<T> const &)
    • un opérateur +(T, Number<T> const &) qui fait appel à l'opérateur + Number<T> const &, Number<T> const &)

    Avec le constructeur de Number<T> marqué explicite pour résoudre le problème des conversions "non souhaitées" (mais souhaitables dans le cas présent)
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. opérateurs arithmétiques / conversions
    Par micamic dans le forum VHDL
    Réponses: 1
    Dernier message: 09/05/2013, 21h22
  2. Conversion implicite et surcharge d'opérateur
    Par davcha dans le forum Langage
    Réponses: 4
    Dernier message: 03/10/2012, 16h54
  3. Opérateur de conversion implicit + dérivation
    Par mister3957 dans le forum C++
    Réponses: 5
    Dernier message: 13/05/2009, 14h50
  4. [C#] Conversion implicite de type object vers int
    Par alexking2005 dans le forum C#
    Réponses: 5
    Dernier message: 02/01/2007, 10h02
  5. [jdbc][oracle] conversion implicite erronée
    Par Jack Huser dans le forum JDBC
    Réponses: 2
    Dernier message: 30/06/2005, 10h23

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo