Developpez.com - Rubrique C++

Le Club des Développeurs et IT Pro

Débat : Lumière sur les rvalue references de C++0x

Le 2009-01-23 16:03:42, par Arzar, Membre émérite
Bonjour!

J'ai accès depuis quelques jours à une config Linux, et j'en profite pour tester le mode experimental c++0x de gcc. J'essaie de comprendre les rvalue references et l'impact qu'elles auront sur notre manière de coder, mais je bute sur deux problèmes :

Question 1

Première essai. Une classe "movable" mais pas copiable
Code :
1
2
3
4
5
6
7
8
9
struct NonCopyable
{
    NonCopyable() = default;
    NonCopyable(NonCopyable&&) = default;
    //empeche la copie
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable& operator=(const NonCopyable&) = delete;
};
Mais ça ne compile pas. Gcc m'annonce qu'il est impossible d'avoir un move contructeur par défaut. Pourquoi cela ?
Ne pourrait pas avoir un move constructeur qui ferait des move membre à membre ?
Code :
1
2
3
4
5
NonCopyable(NonCopyable&& ncp):
membre1(move(ncp.membre1),
membre2(move(ncp.membre2), 
...
Question 2

A l'heure actuelle, pour appliquer un traitement sur un objet lourd, la syntaxe revient toujours plus ou moins à foo(HeavyClass&, Param1, Param2, Param3...). Esthétiquement je préfère de beaucoup la syntaxe HeavyClass foo(Param1, Param2, Param3)... mais il y a la copie.

J'avais cru comprendre que les rvalue references allaient réunir les deux mondes et nous permettre ce genre de chose :
X&& foo();
X x = foo();
Sans copie aucune. \0/

Ben il semble que non.
Code :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
std::vector<std::string>&& parse(const std::string& s, char token)
{
    std::vector<std::string> result;
    std::string::size_type first = 0, last;
    while (first != std::string::npos)
    {
        last = s.find_first_of(token, first);
	result.push_back(s.substr(first, last - first));
	first = s.find_first_not_of(token, last);
    }
    return move(result); // move explicite
}
std::vector<std::string> parse = foo("Le.c++0x.c'est.l'avenir.",'.');
Segmentation fault.
Le mode debug confirme que le destructeur de result est appelé en sortant du scope de parse(), avant le move constructeur, d'où la segmentation fault. Or je croyais que le rôle même de std::move était de prolonger un peu les temporaires pour leur donner le temps de faire les opérations impliquant les rvalue reference et seulement ensuite d'être détruit. Ou cela coince-t-il ?

Merci!
  Discussion forum
39 commentaires
  • Montag
    Membre actif
    Salut,

    Juste une remarque concernant dans ton précédent message, un code de ce type:
    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class X { /*...*/ };
    
    X foo();
    
    int main()
    {
      X x=foo();
    
      return 0;
    }
    ne va pas générer une copie* comme tu as l'air de le croire.

    * à condition bien entendu d'avoir un bon compilateur qui utilise le NRVO
  • loufoque
    Expert confirmé
    Ce n'est pas std::vector<std::string>&& parse(const std::string& s, char token)
    mais std::vector<std::string> parse(const std::string& s, char token).
    Et il n'y a pas besoin de move explicite.

    Par contre, je ne suis pas certain que la bibliothèque standard de GCC soit move-aware...

    Il faut retourner par valeur, sinon tu retournes une réference vers un temporaire...
  • Florian Goo
    Membre éclairé
    Les rvalue references et la move semantics, tout ceci n'est que de la… sémantique ! Autrement dit, cela ne sert qu'à exprimer plus précisément ce que tu attends de ton code.

    En outre, la fonction std::move() n'a rien de magique (elle ne prolonge en aucun cas la durée de vie d'une variable). Elle ne sert qu'à préciser que tu veux te servir d'une variable en tant que rvalue reference (en bref, c'est équivalent à un static_cast). Voici par ailleurs sa définition, qui est très simple :
    Code :
    1
    2
    3
    4
    5
    6
    7
    template <class T>
    typename remove_reference<T>::type&&
    move(T&& a)
    {
        return a;
    }
    Là où c'est intéressant, c'est que cette sémantique te permet de préciser ce que tu veux qu'il se passe quand un objet est envoyé en tant que rvalue reference en paramètre d'une fonction. Par exemple un constructeur (ce sera alors ce qu'on appelle un move constructor). Lorsque tu utilises un move constructor, tu exprimes le fait que l'objet qui est passé en paramètre peut être altéré, vidé, réinitialisé… ça t'est complètement égal, du moment que l'objet construit soit au final égal à l'objet passé en paramètre (ou plutôt sa valeur avant que celui-ci soit altéré, bien sûr).
    Mais un move constructor n'a rien de magique. Les instructions que contiennent ce type de constructeur ne font rien figurer de nouveau qu'on ne connaissait pas en C++98.

    Voici l'exemple le plus parlant : mettons que tu aies une classe clone_ptr, qui contient un pointeur vers une donnée (peu importe le type de cette donnée).
    Dans un copy constructor classique, cette donnée devra être copiée (et ça peut être long, selon la taille de la donnée en question).
    Alors que dans un move constructor, on va se contenter de copier le pointeur qui pointe vers cette donnée. D'autre part, on va réinitialiser la valeur du pointeur de l'objet (passé en paramètre du constructeur) à zéro, histoire qu'il n'y ait pas de conflit. Deux affectations de int, et c'est réglé : c'est donc extrêmement rapide. L'inconvénient, c'est que l'objet passé en paramètre du constructeur a été altéré, mais pas de problème, puisque tu as clairement exprimé le fait que ça t'était égal.
    Voici le code de cette fameuse classe clone_ptr :
    Code :
    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
    template <class T>
    class clone_ptr
    {
    private:
        T* ptr;
    public:
        // construction
        explicit clone_ptr(T* p = 0) : ptr(p) {}
    
        // destruction
        ~clone_ptr() {delete ptr;}
    
        // copy semantics
        clone_ptr(const clone_ptr& p)
            : ptr(p.ptr ? p.ptr->clone() : 0) {}
    
        clone_ptr& operator=(const clone_ptr& p)
        {
            if (this != &p)
            {
                delete ptr;
                ptr = p.ptr ? p.ptr->clone() : 0;
            }
            return *this;
        }
    
        // move semantics
        clone_ptr(clone_ptr&& p)
            : ptr(p.ptr) {p.ptr = 0;}
    
        clone_ptr& operator=(clone_ptr&& p)
        {
            std::swap(ptr, p.ptr);
            return *this;
        }
    
        // Other operations
        T& operator*() const {return *ptr;}
        // ...
    };
    Source : http://www.artima.com/cppsource/rvalue.html

    Maintenant, dans un cas concret d'une classe dont les attributs sont des variables de type primaire et des conteneurs de la STL, tu n'auras pas à faire ce type d'opérations de pointeurs, puisque les classes de la lib standard de C++0x définiront des move constructors et des move assignment operator (surcharge d'operator= prenant une rvalue reference). Tu feras donc un move de ta string ou de ton vector, et la lib standard fera ce qu'il faut.

    Sinon pour répondre directement à tes questions :
    1) Effectivement, tu peux retourner par valeur sans t'inquiéter, renseigne-toi sur la NRVO.
    2) Le comité ISO en cause ici : http://www.open-std.org/jtc1/sc22/wg...008/n2583.html
  • loufoque
    Expert confirmé
    Ton operator=(const clone_ptr& est mauvais.
    Que se passe-t-il si clone lève une exception ?
  • Florian Goo
    Membre éclairé
    Je te renvoie aux auteurs de l'article que j'ai cité, à savoir Howard E. Hinnant, Bjarne Stroustrup et Bronek Kozicki
    Je n'ai pas vraiment analysé l'aspect exception-safe de ce code, d'une part parce que ce sont les messieurs du dessus qui l'ont écrit et d'autre part parce que sa raison d'être est plus pédagogique qu'autre chose.
  • camboui
    Membre éprouvé
    Ces messieurs ne pratiquent donc pas le vénéré "copy and swap" ?
  • Goten
    Membre chevronné
    Apperemment pas... enfin dans ce code à visé didactique. Parce que sinon si j'ai déjà vu des papiers de BS et autres où ils recommandaient bien le copy'n'swap.
  • Médinoc
    Expert éminent sénior
    Le copy-and-swap, dans le cas des rvalue references, ça consiste le plus souvent à faire juste le swap, non?
  • white_tentacle
    Membre émérite
    Ça ne choque que moi de faire du "copy-and-swap" sur un "non-copyable" ?
  • loufoque
    Expert confirmé
    clone_ptr n'est pas noncopyable.
    On parle de son affectation de copie, là.

    Pour les move semantics, un swap n'est pas forcément le meilleur choix puisque l'ancienne ressource ne sera libérée que tardivement.