Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

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

Le , par Arzar

7PARTAGES

0  0 
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 : Sélectionner tout
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 : Sélectionner tout
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 : Sélectionner tout
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!

Une erreur dans cette actualité ? Signalez-le nous !

Avatar de Montag
Membre actif https://www.developpez.com
Le 23/01/2009 à 16:24
Salut,

Juste une remarque concernant dans ton précédent message, un code de ce type:
Code : Sélectionner tout
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
0  0 
Avatar de loufoque
Expert confirmé https://www.developpez.com
Le 23/01/2009 à 18:05
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...
0  0 
Avatar de Florian Goo
Membre éclairé https://www.developpez.com
Le 24/01/2009 à 18:24
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 : Sélectionner tout
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 : Sélectionner tout
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
0  0 
Avatar de loufoque
Expert confirmé https://www.developpez.com
Le 25/01/2009 à 0:38
Ton operator=(const clone_ptr& est mauvais.
Que se passe-t-il si clone lève une exception ?
0  0 
Avatar de Florian Goo
Membre éclairé https://www.developpez.com
Le 25/01/2009 à 0:52
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.
0  0 
Avatar de camboui
Membre éprouvé https://www.developpez.com
Le 27/01/2009 à 10:45
Ces messieurs ne pratiquent donc pas le vénéré "copy and swap" ?
0  0 
Avatar de Goten
Membre chevronné https://www.developpez.com
Le 27/01/2009 à 10:57
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.
0  0 
Avatar de Médinoc
Expert éminent sénior https://www.developpez.com
Le 27/01/2009 à 11:00
Le copy-and-swap, dans le cas des rvalue references, ça consiste le plus souvent à faire juste le swap, non?
0  0 
Avatar de white_tentacle
Membre émérite https://www.developpez.com
Le 27/01/2009 à 11:34
Ça ne choque que moi de faire du "copy-and-swap" sur un "non-copyable" ?
0  0 
Avatar de loufoque
Expert confirmé https://www.developpez.com
Le 27/01/2009 à 14:30
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.
0  0