Partage
  • Partager sur Facebook
  • Partager sur Twitter

Déclaration canonique d'un opérateur binaire,

et lookup

Sujet résolu
    2 octobre 2015 à 14:19:54

    RPGamer a écrit:

    Un gros avantage des fonctions libres c'est le const correctness. Les fonctions membres ne permettent pas d'effectuer des opérations sur un objet constant (pointeur this constant).

    Humm, this est sans doute constant (quand une fonction membre est déclarée constante), mais il n'y a rien qui t'empêche de faire une copie de l'objet courant et de renvoyer cette copie (pas par référence, cependant)

    Mais le vrai gros avantage des fonctions libres est surtout qu'elles permettent de profiter des éventuelles conversions implicites pour les deux opérandes, alors que les fonctions membres ne permettent d'en profiter que pour l'opérande de droite.

    Un petit exemple pour démontrer ce point:  Mettons une classe MyInt qui prenne la forme de

    class MyInt{
    public:
        MyInt(int i = 0):i_(i){}
        int value() const{return i_;}
    private:
        int i_;
    };

    Cette classe permet la conversion implicite d'un int en MyInt, la preuve :

    int main(){
        MyInt a{2};
        a = 5;
        std::cout<<a.value();//affiche 5
        return 0;
    }

    Tu pourrais décider d'autoriser les quatre opérations de base sur cette classe sous une forme proche de

    class MyInt{
    public:
        MyInt(int i = 0):i_{i}{}
        int value() const{return i_;}
        MyInt & operator +=(MyInt const & other){
            i_+=other.value();
            return *this;
        }
        // on pourrait avoir les autres 
    private:
        int i_;
    };

    Avec ceci, tu peux profiter de la conversion implicite sur le deuxième opérande, sous une forme proche de

    int main(){
        MyInt a{2};
        MyInt b{3};
        a+=b; //logique
        a+=7; // conversion implicite de 7 en MyInt
        std::cout<<a.value();
        return 0;
    }
    

    Mais, par contre, tu ne peux pas profiter de la conversion implicite du premier opérande.  Le code suivant ne compilera pas:

    int main(){
        MyInt a{2};
        int c{3};
        c+=a; // impossible de convertir MyInt en in
        return 0;
    }

    Ca va avec  l'opérateur qui fait à la fois l'opération indiquée et l'affectation, car il est "normal" que l'opérande de gauche prenne la valeur correspondant au résultat.

    Par contre, avec l'opérateur qui ne fait pas l'affectation, on s'attend à pouvoir non seulement écrire un code proche de

    int main(){
        MyInt a{2};
        MyInt b  = a + 7;
    
    }

    qui fonctionnerait si l'opérateur + était défini comme fonction membre de MyInt

    mais aussi un code proche de

    int main(){
        MyInt a{2};
        MyInt b  = 7 + a;
    
    }

    Qui poserait problème car, en tant que fonction membre, l'opérateur + s'attend à recevoir... un pointeur de type MyInt comme premier paramètre (implicite) de la fonction (le fameux this).

    Par contre, si on défini l'opérateur + comme une fonction libre, étant donné qu'il y a deux opérandes, nous devons lui fournir... deux paramètres.  Et ces deux paramètres seront... des références constantes sur des objets de type MyInt.  Et ca, ca nous permet de profiter du concept de "variables temporaires anonymes" :

    On crée (par conversion implicite, dans le cas présent) une variable qui n'a pas de nom (anonyme :D) et qui ne sera accessible que "pour toute la durée de l'exécution de la fonction appelée" (elle est donc bien temporaire au niveau de la fonction appelante ;) )

    Ainsi, en ajoutant l'opérateur + en tant que fonction libre, sous la forme de

    MyInt operator + (MyInt const & a, MyInt const & b){
        MyInt copy(a);
        return copy +=b;
    }

    on permet à tout le code qui vient de compiler et de s'exécuter correctement

    int main(){
        MyInt a{2};
        MyInt b{3};
        auto c = a +b;
        c = a + 7;
        std::cout<<c.value()<<"\n";
        c = 10 + b;
        std::cout<<c.value()<<"\n";
        return 0;
    }
    




    • Partager sur Facebook
    • Partager sur Twitter
    Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
      2 octobre 2015 à 14:54:26

      En fait, j'ai vachement creusé la bonne écriture des opérateurs binaires. Et le compilateur nous veut un petit peu plus verbeux que ce que l'on aimerait pour correctement optimiser.

      En gros, j'ai convergé vers: https://github.com/LucHermitte/LucHermitte.github.io/blob/source/source/_posts/codes/test-arithmetic-operators/test-arithmetic-operators.cpp#L82

      Vector operator+(Vector const& lhs, Vector const& rhs) {
          // auto res = Vector(lhs);
          Vector res(lhs);
          res += rhs;
          std::cout << "  before return (const&, const&)\n";
          return res;
      }
      // Plus, bonus en C++11
      Vector operator+(Vector && lhs, Vector const& rhs) {
          lhs += rhs;
          std::cout << "  before return (&&, const&)\n";
          return std::move(lhs);
      }
      Vector operator+(Vector const& lhs, Vector && rhs) {
          rhs += lhs;
          std::cout << "  before return (const&, &&)\n";
          // force move as RVO cannot apply
          return std::move(rhs);
      }
      Vector operator+(Vector && lhs, Vector && rhs) {
          lhs += rhs;
          std::cout << "  before return (&&, &&)\n";
          return std::move(lhs);
      }

      (Oui, il y a des return move et c'est normal. L’élision de copie est empéchée, de même que le return implicite en move à partir du moment où l'on retourne un paramètre qui est un lvalue à l'endroit du return. On peut donc explicitement appeler move(), cela ne va rien empécher qui pouvait être fait donc, mais au moins, il y a bien un déplacement en retour)

      (Et on n'a pas la possibilité de renvoyer une référence sans risquer de renvoyer des dangling references)

      (Et pour compatibilité avec le C++11, la première écriture ne peut pas prendre par copie, sinon on a des ambiguités avec les autres surcharges; et surtout on ne pourrait pas absorber les temporaires à droite avec un op+(V const&, V))

      Pour la symétrie, je pense que RPGamer le savait déjà.

      Si tu reprends mon besoin initial: vendre la liberté + symétrie dans un environnement où de toutes façons les conversions implicites ne peuvent pas avoir lieu (templates obligent). Mais bon. Finalement, c'est passé avec quelques références citées.

      -
      Edité par lmghs 2 octobre 2015 à 14:58:53

      • Partager sur Facebook
      • Partager sur Twitter
      C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.
        5 octobre 2015 à 15:06:42

        Mon exemple était un très mauvais exemple puisque operator -() n'agit PAS sur l'objet en cours. Donc oui, globalement il suffit de déclarer la fonction const, l'objet n'étant pas modifié ça passera la compilation.

        En revanche, je pense que

        Vector operator +(Vector && lhs, Vector &rhs) {
            return lhs += rhs;
        }

        sera automatiquement optimisé vers un déplacement par le compilateur. A vérifier.

        -
        Edité par RPGamer 5 octobre 2015 à 15:24:18

        • Partager sur Facebook
        • Partager sur Twitter
        Don't be serious but do it seriously.
          5 octobre 2015 à 15:29:59

          RPGamer, lance mon code d'exemple que j'ai posé sur le github de mon blog.

          Ton code correspond au cas 1, et j'ai observé avec g++
          - LV + LV -> 2 copy-ctr
          - LV + LV + LV -> 3 copy-ctr
          - LV + LV +LV + LV -> 4 copy-ctr
          - (LV+LV) + LV -> 3 copy-ctr
          - (LV+LV)+(LV+LV) -> 5 copy-ctr
          - LV + f() -> 2 copy-ctr
          - f() + LV -> 1 copy-ctr
          - f() + f() -> 1 copy ctr

          Alors qu'avec le code #9, que j'ai donné au dessus, j'ai à la place, en C++11 (avec g++ et avec clang)

          - LV + LV -> 1 copy-ctr
          - LV + LV + LV -> 1 copy-ctr, 1 move-ctr
          - LV + LV +LV + LV -> 2 copy-ctr, 2 move-ctr
          - (LV+LV) + LV -> 1 copy-ctr, 1 move-ctr
          - (LV+LV)+(LV+LV) -> 2 copy-ctr, 1 move-ctr
          - LV + f() -> 1 move-ctr
          - f() + LV -> 1 move-ctr
          - f() + f() -> 1 move-ctr

          Pour ces choses là, il faut aller plus loin que supposer et tester ce que nous font les compilateurs.

          • Partager sur Facebook
          • Partager sur Twitter
          C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.
            5 octobre 2015 à 16:07:05

            OK donc faudra faire

            Vector operator +(Vector && lhs, Vector &rhs) {
                return std::move(lhs += rhs);
            }

            parce que GCC est pas capable de le faire seul ? J'ai lu l'inverse d'où ma supposition mais je ne l'avais jamais testé en effet, d'où mon "à vérifier". En tout cas tes tests sont intéressants.

            • Partager sur Facebook
            • Partager sur Twitter
            Don't be serious but do it seriously.
              5 octobre 2015 à 16:07:14

              Il y a une raison bête à cela: operator+= retourne une référence. Niet pour les déplacement et l’élision.

              • Partager sur Facebook
              • Partager sur Twitter
                5 octobre 2015 à 16:16:15

                Oui il retourne une référence qui sert ensuite pour créer un nouveau Vector car ce n'est pas une référence qui est retournée.

                Qu'en est-il du retour d'un objet anonyme?

                Vector Vector::operator /(Vector &vector) const
                {
                    return Vector(vector); 
                }

                Je sais, cet opérateur ne fait rien d'intelligent. Je pense que ça dépend fortement du compilo : https://en.wikipedia.org/wiki/Return_value_optimization

                -
                Edité par RPGamer 5 octobre 2015 à 16:17:24

                • Partager sur Facebook
                • Partager sur Twitter
                Don't be serious but do it seriously.
                  5 octobre 2015 à 16:31:01

                  Il n'y a qu'une chose à faire -> écrire des tests. => cf mon code publié
                  • Partager sur Facebook
                  • Partager sur Twitter
                  C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.
                    5 octobre 2015 à 16:50:44

                    Aucun déplacement implicite avec GCC 4.9.2 en tout cas.
                    • Partager sur Facebook
                    • Partager sur Twitter
                    Don't be serious but do it seriously.

                    Déclaration canonique d'un opérateur binaire,

                    × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
                    × Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
                    • Editeur
                    • Markdown