L'opérateur && vs const type&

L'intérêt d'utiliser l'opérateur &&

a marqué ce sujet comme résolu.

Bonjour,

Je n'arrive toujours pas à trouver l'intérêt d'utiliser l'opérateur && introduit en C++11. Pour vous montrer un exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;

class Toto {
    public:
    void meth(const int& a){
        cout<<"Je suis meth(const int& a)"<<endl;
        cout<<a<<endl;
    }

    void meth(int&& a){
        cout<<"Je suis meth(int&& a)"<<endl;
        cout<<a<<endl;          
    }
};

int main() {
    Toto* t = new Toto();
    t->meth(123); // Va afficher Je suis meth(int&& a)
    delete t;
    return 0;
}

La méthode possédant la signature "meth(int&& a)" est prioritaire sur "meth(const int& a)", cependant si on la supprime, c'est l'autre qui prend la main.

  1. Vous ne pensez pas que c'est "dangereux" dans le sens où on peut exécuter une implémentation ayant la signature que l'on pense être bon mais sans même comprendre pendant des heures pourquoi on n'a pas le bon résultat parce qu'en faite c'était l'autre méthode qu'on aurait voulu lancer ?
  2. Quel est le réel intérêt/enjeu de cet opérateur && ? Je veux dire, le plus que cela apporte, la "chose" qu'on ne pouvait pas faire avant et que c'est désormais possible aujourd'hui.
  3. Quels sont les cas où il faut privilégier l'un et pas l'autre ?

Merci :)

+0 -0

Pour ce que j'en sais, la syntaxe "T&&" sert pour les "move constructor", c'est à dire lorsque tu vas copier le contenu d'un objet sur le point d'être détruit. La copie est couteuse, et inutile, puisque l'objet copié sera détruit. Donc autant réutiliser l'espace mémoire qui lui avait été consacré : plutôt que de copier les données, on copie les pointeurs. Il va s'en dire que cette optimisation n'est réellement intéressante que pour des structures de données comme des vector, où il y a un réel gain à ne recopier que le pointeur pointant sur le tableau et à laisser le tableau inchangé en mémoire.

Des explications (en anglais) ici : http://en.cppreference.com/w/cpp/language/move_constructor

La plupart du temps l'implémentation automatique de ton compilateur du "move constructor" sera efficace, pour peu que ton objet soit un agrégat de POD ou d'objets implémentant un move constructor.

Quant aux autres utilisations, j'avoue n'avoir jamais vraiment compris moi non plus. Je serais curieux de voir l'avis des spécialistes du C++ sur le sujet.

EDIT: ce post répond à toutes tes questions :

http://stackoverflow.com/questions/5481539/what-does-t-double-ampersand-mean-in-c11

+0 -0

&& permet de matcher une rvalue-reference. Et ce que tu vas matcher, tu auras un alias dessus, et tu pourras le modifier – à contrario, le const& ne permet pas les modifications.

Ensuite, ce n'est qu'une référence. Aucun déplacement n'a lieu. Les déplacements ont lieu en construction et en affectation (parce qu'on les aura programmé!!!), et dans certaines fonctions qui procèderont à un déplacement (si on cherche bien, elles finissent par appeler un constructeur ou un opérateur d'affectation qui déplace -> (emplace|push)_(back()|front()|0).

Après, il y a deux camps:

  • avec Scott Meyers en tête, qui pense qu'une fonction qui reçoit un paramètre seulement déplaçable (p.ex. std::unique_ptr<>) doit indiquer dans sa signature que la fonction s'appropriera le paramètre avec un &&. NB: c'est ainsi qu'est écrite la SL v11, cf. les fonctions susnommées – mais il est vrai que ce n'est pas restreint aux paramètres seulement déplaçables.

  • Tous les autres qui disent que cela n'assure pas le déplacement (chose que tu observes), cela permet juste le matching. Et de fait, prendre par && limite les possibilités d'expression que le langage nous offre. Ainsi, pour eux une fonction puits pour un paramètre seulement déplaçable devrait recevoir le paramètre par copievaleur. S.M. oppose que cela induira des baisses de performances sur les types dont le volume caché n'est pas négligeable par rapport à la surface émergée (genre un truc qui contient un std::array). Je pense qu'il préfère avoir une écriture sémantiquement forte qui dit "puits" et ne laisse aucun doute dès la première lecture.

Perso, je tends à préférer le camp de la sémantique claire (quitte à restreindre les possibilités d'expression, chose que l'on fait tout le temps quand on se force à respecter des bonnes pratiques (genre ne pas dériver publiquement une liste triée d'une liste).

Je sens que c'est le genre de débat du même acabit que NULL vs 0 : les experts ne sont pas d'accord entre eux car techniquement la syntaxique implique des choses, mais que sémantiquement on en voudrait d'autres.

Pour répondre plus précisément à tes questions 1 et 3, je t'invite à lire les docs de Scott Meyers sur les références universelles (maintenant appelées forwarding references)

Merci pour vos retours,

@lmghs, j'ai déjà lu l'article de Scott Meyers (lien) mais je prends souvent du temps à comprendre certaines choses très subtiles comme c'est le cas ici. C'est pour ça que j'essaye de poser des questions assez précises, c'est parce que je ne saisi pas forcément la subtilité des infos que je trouve.

@Ge0, merci pour le lien, je pense mieux comprendre l'idée car on représente bien la différence dans les exemples : dans le cas avec le constructeur const& on crée forcément une "copie" si on veut faire les choses bien & simple car la source d'origine de l'objet qu'on duplique continuera d'exister hors-scope, tandis qu'avec le constructeur && on "déplace" la source d'origine (donc pas de copie) et comme il n'y a qu'une seule source (à la manière d'un unique_ptr je dirais vulgairement) il ne devrait pas y avoir de souci.

4 - N'hésitez pas à me dire si je me plante dans ma compréhension.

5 - Que pensez vous de l'usage de l'opérateur && dans un environnement multi-thread ?

6 - En quoi consiste exactement le "déplacement" au niveau de la mémoire ? Est-ce qu'on déplace un bloc de registres vers une autre zone ? (comme un couper/coller je dirais vulgairement).

7 - Existe-t-il des benchmarks pour std::forward() et std::move() prouvant que c'est plus efficace que de faire des copies ?

Merci :)

+0 -0

5- Pas de rapport direct. On n'est pas censés déplacer des variables (lvalues), mais des rvalues (résultat d'expression, ou qui vont mourir).

Si tu déplaces une variable pouvant continuer à être pointée par d'autres, tu va avoir des soucis. Même problèmatiques que unique VS shared.

6- Tu copies la partie émergée de l'iceberg, donne la partie immergée, et dis que la partie immergé de l'iceberg dépossédé est minimaliste et juste suffisante pour permettre destruction et affectation.

7- move et forward ne sont que des castings. Ils ne font rien si ce n'est de permettre de changer le statut de lvalue en rvalue (et un peu plus pour forward). Le prix qui compte est celui des deux parties du iceberg. Il y a eu un article il n'y a pas longtemps sur ces sujets. Je crois que ce sont ceux là :

Il y a un point, discutable, qui me fait pencher vers && plutôt qu'un passage par valeur quand le cas d'utilisation est foo(std::move(a)).

En soumettant les deux solutions aux critères de résistance aux exceptions, j'aurais tendance à préférer celle qui permet d'avoir une résistance forte. Cependant j'ai du mal à définir exactement ce qu'est la résistance forte dans le cas d'une instruction telle que foo(bar()), si foo lance une exception, est-ce que l'état doit être celui avant ou après l'execution de bar ?

Dans le fond on retrouve un peu la problématique qu'il y a avec std::unique_ptr et new, il est difficile (dans le cas générale) de revenir en arrière lorsqu'il y a plusieurs appels en une instruction (ce qui conduit à une fuite mémoire dans ce cas).

Dans le cas générale je pense qu'il est impossible d'écrire les fonctions foo et bar de manière à ce qu'une exception dans l'instruction foo(bar()) laisse l'état qu'il y avait avant cette instruction.

Par contre, dans le cas de foo(std::move(a)), c'est tout à fait possible à condition que le passage soit fait par && et non par valeur (le code de foo peut réaffecter la référence au besoin, ce qu'il ne peut pas faire si la copie à vidé le contenu de la rvalue).

1
2
3
4
5
6
7
8
9
auto ptr = /*...*/; //Un unique_ptr
try
{
  foo(std::move(ptr));
}
catch(/*...*/)
{
  //Est-ce qu'ici il ne serait pas intéressant d'avoir `ptr` tel qu'il était avant l'appel à foo ?
}
+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte