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 :

[C++0x] Move semantics + NRVO


Sujet :

Langage C++

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre chevronné
    Avatar de Florian Goo
    Profil pro
    Inscrit en
    Septembre 2008
    Messages
    680
    Détails du profil
    Informations personnelles :
    Âge : 39
    Localisation : France, Paris (Île de France)

    Informations forums :
    Inscription : Septembre 2008
    Messages : 680
    Par défaut [C++0x] Move semantics + NRVO
    Bonjour les développeurs,

    Soit le code suivant :
    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
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
     
    #include <iostream>
     
    class movable
    {
    	public:
    		movable(int){}
     
    		movable(const movable&) = delete;
     
    		movable(movable&&)
    		{
    			std::cout << "move\n";
    		}
     
    		const movable&
    		operator=(const movable&) = delete;
     
    		const movable&
    		operator=(movable&&) = delete;
    };
     
    movable
    f()
    {
    	return movable(0);
    }
     
    int
    main()
    {
    	std::cout << "test1:\n";
    	movable o1 = f();
     
    	std::cout << "test2:\n";
    	movable o2(f());
     
    	std::cout << "test3:\n";
    	movable o3(std::move(f()));
     
    	return 0;
    }
    On a une classe non copiable mais movable et une fonction qui renvoie un objet de cette classe. Dans le main(), j'effectue trois appels à cette fonction, en effectuant respectivement une assignation par copie, une construction par copie et une construction par mouvement.

    Sachant que j'ai désactivé la construction par copie et l'opérateur d'assignation, je m'attends à ce que le compilateur refuse les tests 1 et 2. Seulement non, aucun problème de compilation.
    Étant donné que j'ai la sortie suivante, j'imagine que c'est la NRVO qui permet ce comportement :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
     
    test1:
    test2:
    test3:
    move
    Si je désactive la NRVO (-fno-elide-constructors), des déplacements sont effectués dans tous les cas :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     
    test1:
    move
    move
    test2:
    move
    move
    test3:
    move
    move
    On a donc un code qui compile seulement dans le cas où le compilateur supporte la NRVO.
    C'est pas un peu discutable comme comportement ? À moins que cette optimisation ait été prise en compte dans le nouveau standard ?

    J'hésite à remonter ce comportement comme un bug. Vous en pensez quoi ?


    Voici le Makefile pour ceux qui veulent tester (GCC 4.4 requis) :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
     
    test: main.cpp 
    	g++-4.4 -o test main.cpp -W -Wall -ansi -pedantic -std=c++0x -fno-elide-constructors
    Cours : Initiation à CMake
    Projet : Scalpel, bibliothèque d'analyse de code source C++ (développement en cours)
    Ce message a été tapé avec un clavier en disposition bépo.

  2. #2
    Expert confirmé
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Décembre 2003
    Messages
    3 549
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Décembre 2003
    Messages : 3 549
    Par défaut
    On a donc un code qui compile seulement dans le cas où le compilateur supporte la NRVO.
    Ton code compile dans les deux cas.
    Je vois pas où est le problème. Dans le premier cas les déplacements sont évités par l'optimisation NRVO, dans le second elles ne le sont pas, ce qui est tout à fait normal.

    On a une classe non copiable mais movable et une fonction qui renvoie un objet de cette classe. Dans le main(), j'effectue trois appels à cette fonction, en effectuant respectivement une assignation par copie, une construction par copie et une construction par mouvement.
    Le problème, c'est que f() est une rvalue... Tes trois tests sont en fait équivalents. (sauf que le déplacement explicite de std::move n'est pas éliminable par NRVO)
    Par ailleurs, tu n'effectues aucune assignation.

  3. #3
    Membre Expert

    Inscrit en
    Mai 2008
    Messages
    1 014
    Détails du profil
    Informations forums :
    Inscription : Mai 2008
    Messages : 1 014
    Par défaut
    Oui, loufoque a raison.
    Ta fonction f revoie un "movable" temporaire, c'est une dire une rvalue. Donc les deux premiers test font la même chose et le troisième est un peu bizarre vu que le std::move prend une rvalue de type "movable" et qu'il renvoie une référence sur cette rvalue (un movable&&) sans qu'on sache bien à quoi ça sert. Le constructeur par mouvement se chargera tout seul de prendre une référence sans qu'on est besoin de lui donner explicitement. Exactement comme avec les lvalue references d'ailleurs.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
     
    class T
    {
       T(const T&);
    }
     
    T& copy(T t)
    {
       static_cast<T&>(t);
    }
     
    T t1;
    T t2 = copy(t); // pas besoin !
    T t3 = t; // ok
    En résumé :
    T : une valeur. Elle peut être soit une lvalue, soit une rvalue.
    lvalue : qui peut se mettre à gauche de l'opérateur = et qui a un nom.
    Dans ce code, t est une lvalue :
    rvalue : qui peut se mettre à droite de l'opérateur = et qui n'a pas de nom (car temporaire)
    Dans ce code la valeur de retour de f est une rvalue (et je suis obligé de dire "valeur de retour de f", car elle n'a pas de nom.)
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
     
    T f()
    {
       T t;
       return t;
    }
    T& : référence sur une lvalue. Ne peut se binder qu'avec des lvalue.
    T&& : référence sur une rvalue. Ne peut se binder qu'avec des rvalue.

    Ce qui veut dire que :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
     
    T&& f()
    {
       T t;
       return t;
    }
    est une erreur au même titre que :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
     
    T& f()
    {
       T t;
       return t;
    }
    Dans les deux cas, f renvoie une référence sur la valeur locale t.
    D'ailleurs gcc se plaint "warning: reference to local variable 't' returned"

    On a donc un code qui compile seulement dans le cas où le compilateur supporte la NRVO.
    Non, ce qui se passe ici, c'est que l'optimisation NRVO court-circuite complètement le constructeur par copie et/ou le constructeur par mouvement, et construit directement la variable d'arrivé en une seul fois. C'est le cas optimal.

  4. #4
    Membre Expert
    Avatar de Goten
    Profil pro
    Inscrit en
    Juillet 2008
    Messages
    1 580
    Détails du profil
    Informations personnelles :
    Âge : 34
    Localisation : France

    Informations forums :
    Inscription : Juillet 2008
    Messages : 1 580
    Par défaut
    Question qui sort un peu du contexte mais tant qu'a faire :
    Ici le mot clef delete a quel impact, a quoi sert il dans ce contexte ?


    A noter que les deux premiers test font appel au constructeur de recopie... Il y'a pas d'affectation quoi.

  5. #5
    Membre Expert

    Inscrit en
    Mai 2008
    Messages
    1 014
    Détails du profil
    Informations forums :
    Inscription : Mai 2008
    Messages : 1 014
    Par défaut
    Delete permet d'officialiser l'astuce qui consistait à déclarer le constucteur par copie en private sans le définir, histoire que le linker se gauffre si quelqu'un tente de faire une copie. Donc d'interdire la copie. C'était un peu de la bidouille quand même.

    L'avantage de delete, c'est que l'erreur à lieu à la compilation, et que le message d'erreur est beaucoup moins cryptique qu'une erreur de linker.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class noncopyable()
    {
    public:
       noncopyable() = default;
       noncopyable(const noncopyable&) = delete;
    } 
     
    noncopyable n1;
    noncopyable n2(n1);
    error: deleted function 'noncopyable::noncopyable(const noncopyable&)'
    error: used here (pointe la ligne "noncopyable n2(n1);" )

  6. #6
    Membre Expert
    Avatar de Goten
    Profil pro
    Inscrit en
    Juillet 2008
    Messages
    1 580
    Détails du profil
    Informations personnelles :
    Âge : 34
    Localisation : France

    Informations forums :
    Inscription : Juillet 2008
    Messages : 1 580
    Par défaut
    Hum oki merci . Encore un truc :
    T'aurais un bon article sur les move semantics? Parce que bon plusieurs fois que je tente cette feature de la prochaine norme mais j'ai du mal a voir l'intérêt. Notamment ici permettre de faire des choses qu'on a a première vue interdit. ( En déclarant l'op= et le ctor par recopie privée).

  7. #7
    Membre Expert

    Inscrit en
    Mai 2008
    Messages
    1 014
    Détails du profil
    Informations forums :
    Inscription : Mai 2008
    Messages : 1 014
    Par défaut
    T'aurais un bon article sur les move semantics?
    Le meilleur article que je connaisse sur la question, c'est celui-ci (attention, c'est du costaud, il fait presque trente pages ! A digérer petit à petit.)
    Le seul problème, c'est qu'il a été écrit avant un changement assez important sur la manière dont les références peuvent se binder.

    En résumé :

    Avant : règles hyper complexes, détaillées dans l'article.

    Entre temps, des problèmes de sécurité ont été découverts du à ces règles (si je me souviens bien, il pouvait y avoir dans certain cas des move silencieux).

    Maintenant : règle beaucoup plus strictes, et beaucoup plus simple, à savoir :
    Les reference & ne peuvent se binder qu'avec des lvalue.
    Les reference && ne peuvent se binder qu'avec des rvalues.
    Aucune conversion implicite n'est autorisée.

    Parce que bon plusieurs fois que je tente cette feature de la prochaine norme mais j'ai du mal a voir l'intérêt.
    Je suis un peu dans le même cas. La plupart des articles se focalise beaucoup sur le coté technique. Il est difficile de se faire une idée des pratiques qui émergeront de tout ça.

    D'ailleurs je suis curieux. Florian ? Pourrais-tu détailler un peu dans quel cadre tu utilises la sémantique de mouvement ? Un exemple réel serait surement plus parlant.... Car de mon coté je n'arrive pas vraiment à imaginer des cas d'utilisation où l'on aurait besoin de "transférer" des objets non-copiables.

  8. #8
    Membre chevronné
    Avatar de Florian Goo
    Profil pro
    Inscrit en
    Septembre 2008
    Messages
    680
    Détails du profil
    Informations personnelles :
    Âge : 39
    Localisation : France, Paris (Île de France)

    Informations forums :
    Inscription : Septembre 2008
    Messages : 680
    Par défaut
    Merci pour vos réponses

    Ouch, oui, pour le second cas j'avais effectivement un doute, mais je me suis surtout laissé induire en erreur par le premier cas. J'avais oublié qu'il ne s'agit absolument pas d'une affectation…

    Je suis également déstabilisé par ce type de code (toujours avec la même classe movable) :
    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
     
    void
    f(movable&& m)
    {
    	movable m2(m); //(A)
    }
     
    int
    main()
    {
    	movable m(0);
    	f(m); //(B)
     
    	return 0;
    }
    Ce code ne compile pas, non pas à cause de la ligne (B), mais de la ligne (A) :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    main.cpp: In function ‘void f(movable&&)’:
    main.cpp|8| erreur: deleted function ‘movable::movable(const movable&)’
    main.cpp|25| erreur: used here
    Le code correct est donc celui-ci :
    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
     
    void
    f(movable&& m)
    {
    	movable m2(std::move(m));
    }
     
    int
    main()
    {
    	movable m(0);
    	f(m);
     
    	return 0;
    }
    1°) Le m de main() n'est pas un temporaire (donc pas une rvalue, si j'ai bien tout suivi), pourtant f() l'accepte tel quel.
    Je suppose que cela s'explique par le fait qu'il n'y a aucune surcharge de f() prenant en paramètre une lvalue-reference (le cas échéant, ce serait cette surcharge qui serait appelée). Le test que je viens d'effectuer semble confirmer cette explication.

    2°) Le m de f() est explicitement une référence de rvalue, pourtant il faut faire un std::move() pour que le compilateur comprenne qu'on cherche à appeller le constructeur par déplacement et non le constructeur par copie. Comment expliquer cela ?


    Ahlala, ces rvalue-references… parfois j'ai l'impression d'avoir tout compris, et la seconde d'après je me laisse totalement dérouter !


    Edit : J'ai commencé à taper ma réponse avant d'avoir pu lire ton dernier message, Arzar. Je m'attèle de suite à te trouver un exemple concret !
    Cours : Initiation à CMake
    Projet : Scalpel, bibliothèque d'analyse de code source C++ (développement en cours)
    Ce message a été tapé avec un clavier en disposition bépo.

  9. #9
    yan
    yan est déconnecté
    Rédacteur
    Avatar de yan
    Homme Profil pro
    Ingénieur expert
    Inscrit en
    Mars 2004
    Messages
    10 035
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France, Ille et Vilaine (Bretagne)

    Informations professionnelles :
    Activité : Ingénieur expert
    Secteur : High Tech - Multimédia et Internet

    Informations forums :
    Inscription : Mars 2004
    Messages : 10 035
    Par défaut
    Citation Envoyé par Goten Voir le message
    Parce que bon plusieurs fois que je tente cette feature de la prochaine norme mais j'ai du mal a voir l'intérêt.
    (Si je me trompe pas) Cela aura une utilité avec les algorithms.
    Par exemple le std::sort qui utilise std::swap. Seulement par défaut le swap utilise des copies.
    Actuellement, une solution est de spécialisé le std::swap pour ta classe.
    Avec move, y aura rien à faire.

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

Discussions similaires

  1. "move semantic" testée avec gcc tdm 4.8.1
    Par keitaro42300 dans le forum C++
    Réponses: 4
    Dernier message: 16/12/2013, 00h09
  2. Disponibilité des "move-semantics" ?
    Par cob59 dans le forum Langage
    Réponses: 8
    Dernier message: 17/06/2013, 20h10
  3. Réponses: 6
    Dernier message: 06/06/2013, 11h23
  4. move semantic et attributs const
    Par CedricMocquillon dans le forum C++
    Réponses: 12
    Dernier message: 13/09/2009, 20h34
  5. alter table move
    Par jokos2000 dans le forum Oracle
    Réponses: 7
    Dernier message: 15/06/2005, 13h30

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