Qu'est-ce que std :: move () et quand doit-il être utilisé?

656
  1. Qu'Est-ce que c'est?
  2. Qu'est ce que ça fait?
  3. Quand faut-il l'utiliser?

De bons liens sont appréciés.

Basilevs
la source
43
Bjarne Stroustrup explique le déménagement dans une brève introduction aux références
Rvalue
12
Cette question fait référence à std::move(T && t); il existe également un std::move(InputIt first, InputIt last, OutputIt d_first)qui est un algorithme lié à std::copy. Je le signale afin que les autres ne soient pas aussi confus que moi quand j'ai été confronté pour la première fois à std::movetrois arguments. en.cppreference.com/w/cpp/algorithm/move
josaphatv

Réponses:

287

Page Wikipedia sur les références de valeur R C ++ 11 et les constructeurs de déplacement

  1. En C ++ 11, en plus de copier des constructeurs, les objets peuvent avoir des constructeurs de déplacement.
    (Et en plus de copier les opérateurs d'affectation, ils ont des opérateurs d'affectation de déplacement.)
  2. Le constructeur de déplacement est utilisé à la place du constructeur de copie, si l'objet a le type "rvalue-reference" ( Type &&).
  3. std::move() est un transtypage qui produit une rvalue-référence à un objet, pour permettre son déplacement.

C'est une nouvelle façon C ++ d'éviter les copies. Par exemple, en utilisant un constructeur de déplacement, un std::vectorpourrait simplement copier son pointeur interne vers des données vers le nouvel objet, laissant l'objet déplacé dans un état déplacé de, donc ne copiant pas toutes les données. Ce serait C ++ - valide.

Essayez de googler pour la sémantique des mouvements, la valeur r, la transmission parfaite.

Scharron
la source
40
La sémantique de déplacement requiert que l'objet déplacé reste valide , ce qui n'est pas un état incorrect. (Justification: Il doit encore détruire, faire fonctionner.)
GManNickG
13
@GMan: eh bien, il doit être dans un état sûr à détruire, mais, AFAIK, il ne doit pas être utilisable pour autre chose.
Zan Lynx
8
@ZanLynx: D'accord. Notez que la bibliothèque standard requiert en outre que les objets déplacés soient assignables, mais ce n'est que pour les objets utilisés dans stdlib, pas une exigence générale.
GManNickG
25
-1 "std :: move () est la façon C ++ 11 d'utiliser la sémantique de déplacement" Veuillez corriger cela. std::move()n'est pas la façon d'utiliser la sémantique de déplacement, la sémantique de déplacement est effectuée de manière transparente pour le programmeur. moveil s'agit uniquement d'un transtypage pour passer une valeur d'un point à un autre où la valeur l d'origine ne sera plus utilisée.
Manu343726
15
J'irais plus loin. std::movelui-même ne fait "rien" - il n'a aucun effet secondaire. Il signale simplement au compilateur que le programmeur ne se soucie plus de ce qui arrive à cet objet. c'est-à-dire qu'il donne la permission à d'autres parties du logiciel de se déplacer de l'objet, mais il ne nécessite pas qu'il soit déplacé. En fait, le destinataire d'une référence rvalue n'a pas à faire de promesses quant à ce qu'il fera ou ne fera pas avec les données.
Aaron McDaid
241

1. "Qu'est-ce que c'est?"

Bien que ce std::move() soit techniquement une fonction - je dirais que ce n'est pas vraiment une fonction . C'est une sorte de convertisseur entre les façons dont le compilateur considère la valeur d'une expression.

2. "Que fait-il?"

La première chose à noter est que std::move() rien ne bouge réellement . Il convertit une expression qui est une lvalue (telle qu'une variable nommée) en une xvalue . Une valeur x indique au compilateur:

Vous pouvez me piller, déplacer tout ce que je tiens et l'utiliser ailleurs (car je vais bientôt être détruit) ".

en d'autres termes, lorsque vous utilisez std::move(x), vous autorisez le compilateur à cannibaliser x. Ainsi, si xa, disons, son propre tampon en mémoire - après std::move()ing le compilateur peut avoir un autre objet le posséder à la place.

Vous pouvez également passer d'une valeur (telle qu'une valeur temporaire que vous passez), mais cela est rarement utile.

3. "Quand faut-il l'utiliser?"

Une autre façon de poser cette question est la suivante: "Pourquoi cannibaliser les ressources d'un objet existant?" eh bien, si vous écrivez du code d'application, vous ne vous amuseriez probablement pas beaucoup avec des objets temporaires créés par le compilateur. Donc, vous le feriez principalement dans des endroits comme les constructeurs, les méthodes d'opérateur, les fonctions de type algorithme de bibliothèque standard, etc. Bien sûr, ce n'est qu'une règle d'or.

Une utilisation typique consiste à «déplacer» des ressources d'un objet à un autre au lieu de les copier. @Guillaume renvoie à cette page qui a un court exemple simple: échanger deux objets avec moins de copie.

template <class T>
swap(T& a, T& b) {
    T tmp(a);   // we now have two copies of a
    a = b;      // we now have two copies of b (+ discarded a copy of a)
    b = tmp;    // we now have two copies of tmp (+ discarded a copy of b)
}

utiliser move vous permet d'échanger les ressources au lieu de les copier:

template <class T>
swap(T& a, T& b) {
    T tmp(std::move(a));
    a = std::move(b);   
    b = std::move(tmp);
}

Pensez à ce qui se passe quand T, disons, vector<int>de taille n. Dans la première version, vous lisez et écrivez 3 * n éléments, dans la deuxième version, vous lisez et écrivez simplement les 3 pointeurs vers les tampons des vecteurs, plus les 3 tailles des tampons. Bien sûr, la classe Tdoit savoir comment bouger; votre classe doit avoir un opérateur d'affectation de mouvement et un constructeur de mouvement pour que la classe Tfonctionne.

einpoklum
la source
3
Pendant longtemps, j'ai entendu parler de ces sémantiques de mouvement, je ne les ai jamais examinées. D'après cette description que vous avez donnée, il semble que ce soit une copie peu profonde au lieu d'une copie complète.
Poisson zèbre
7
@TitoneMaurice: Sauf que ce n'est pas une copie - car la valeur d'origine n'est plus utilisable.
einpoklum
3
@ Poisson zèbre, vous ne pourriez pas vous tromper davantage. Une copie superficielle laisse l'original dans le même état exact, un mouvement entraîne généralement l'original vide ou dans un état par ailleurs valide.
rubenvb
17
@rubenvb Zebra n'a pas tout à fait tort. Bien qu'il soit vrai que l'objet cannabilisé d'origine est généralement délibérément saboté pour éviter des erreurs déroutantes (par exemple, définissez ses pointeurs sur nullptr pour signaler qu'il ne possède plus les pointes), le fait que l'ensemble du mouvement est mis en œuvre en copiant simplement un pointeur de la source à la destination (et en évitant délibérément de faire quoi que ce soit avec la pointe) rappelle en effet une copie peu profonde. En fait, j'irais jusqu'à dire qu'un mouvement est une copie superficielle, suivie éventuellement d'une autodestruction partielle de la source. (suite)
Courses de légèreté en orbite
3
(suite) Si nous permettons cette définition (et je l'aime plutôt), alors l'observation de @ Zebrafish n'est pas fausse, juste légèrement incomplète.
Courses de légèreté en orbite
146

Vous pouvez utiliser move lorsque vous devez "transférer" le contenu d'un objet ailleurs, sans faire de copie (c'est-à-dire que le contenu n'est pas dupliqué, c'est pourquoi il pourrait être utilisé sur certains objets non copiables, comme un unique_ptr). Il est également possible pour un objet de prendre le contenu d'un objet temporaire sans faire de copie (et gagner beaucoup de temps), avec std :: move.

Ce lien m'a vraiment aidé:

http://thbecker.net/articles/rvalue_references/section_01.html

Je suis désolé si ma réponse arrive trop tard, mais je cherchais également un bon lien pour le std :: move, et j'ai trouvé les liens ci-dessus un peu "austères".

Cela met l'accent sur la référence r-value, dans quel contexte vous devez les utiliser, et je pense que c'est plus détaillé, c'est pourquoi j'ai voulu partager ce lien ici.

Guillaume
la source
26
Joli lien. J'ai toujours trouvé l'article wikipedia et les autres liens que je suis tombé sur plutôt confus car ils ne font que vous jeter des faits, vous laissant le soin de comprendre quelle est la véritable signification / justification. Alors que "déplacer la sémantique" dans un constructeur est assez évident, tous ces détails sur le passage des valeurs && - ne le sont pas ... donc la description de style tutoriel était très agréable.
Christian Stieber
66

Q: Qu'est-ce que c'est std::move?

A: std::move()est une fonction de la bibliothèque standard C ++ pour la conversion en une référence rvalue.

Simplement std::move(t)équivaut à:

static_cast<T&&>(t);

Une valeur r est une valeur temporaire qui ne persiste pas au-delà de l'expression qui la définit, comme un résultat de fonction intermédiaire qui n'est jamais stocké dans une variable.

int a = 3; // 3 is a rvalue, does not exist after expression is evaluated
int b = a; // a is a lvalue, keeps existing after expression is evaluated

Une implémentation de std :: move () est donnée dans N2027: "Une brève introduction aux références Rvalue" comme suit:

template <class T>
typename remove_reference<T>::type&&
std::move(T&& a)
{
    return a;
}

Comme vous pouvez le voir, std::moveretourne T&&peu importe s'il est appelé avec une valeur ( T), un type de référence ( T&) ou une référence rvalue ( T&&).

Q: Que fait-il?

R: En tant que distribution, il ne fait rien pendant l'exécution. Il est uniquement pertinent au moment de la compilation d'indiquer au compilateur que vous souhaitez continuer à considérer la référence comme une valeur r.

foo(3 * 5); // obviously, you are calling foo with a temporary (rvalue)

int a = 3 * 5;
foo(a);     // how to tell the compiler to treat `a` as an rvalue?
foo(std::move(a)); // will call `foo(int&& a)` rather than `foo(int a)` or `foo(int& a)`

Ce qu'il ne fait pas :

  • Faites une copie de l'argument
  • Appelez le constructeur de copie
  • Changer l'objet argument

Q: Quand faut-il l'utiliser?

R: Vous devez utiliser std::movesi vous souhaitez appeler des fonctions qui prennent en charge la sémantique de déplacement avec un argument qui n'est pas une rvalue (expression temporaire).

Cela me pose les questions de suivi suivantes:

  • Qu'est-ce que la sémantique des mouvements? Déplacer la sémantique contrairement à copier la sémantique est une technique de programmation dans laquelle les membres d'un objet sont initialisés en «prenant le relais» au lieu de copier les membres d'un autre objet. Une telle «prise en charge» n'a de sens qu'avec des pointeurs et des descripteurs de ressources, qui peuvent être transférés à moindre coût en copiant le pointeur ou le descripteur d'entier plutôt que les données sous-jacentes.

  • Quels types de classes et d'objets prennent en charge la sémantique de déplacement? C'est à vous, en tant que développeur, d'implémenter la sémantique des mouvements dans vos propres classes si celles-ci ont intérêt à transférer leurs membres au lieu de les copier. Une fois que vous aurez implémenté la sémantique de déplacement, vous bénéficierez directement du travail de nombreux programmeurs de bibliothèque qui ont ajouté la prise en charge de la gestion efficace des classes avec la sémantique de déplacement.

  • Pourquoi le compilateur ne peut-il pas le comprendre par lui-même? Le compilateur ne peut pas simplement appeler une autre surcharge d'une fonction sauf si vous le dites. Vous devez aider le compilateur à choisir si la version régulière ou la fonction de déplacement doit être appelée.

  • Dans quelles situations voudrais-je dire au compilateur qu'il doit traiter une variable comme une valeur r? Cela se produira très probablement dans les fonctions de modèle ou de bibliothèque, où vous savez qu'un résultat intermédiaire pourrait être récupéré.

Christopher Oezbek
la source
2
Big +1 pour les exemples de code avec sémantique dans les commentaires. Les autres meilleures réponses définissent std :: move en utilisant "move" lui-même - ne clarifie vraiment rien! --- Je pense qu'il convient de mentionner que le fait de ne pas faire de copie de l'argument signifie que la valeur d'origine ne peut pas être utilisée de manière fiable.
ty
34

std :: move lui-même ne fait pas grand-chose. Je pensais qu'il appelait le constructeur déplacé pour un objet, mais il effectue simplement un transtypage de type (transtypage d'une variable lvalue en rvalue afin que ladite variable puisse être passée en argument à un constructeur de déplacement ou à un opérateur d'affectation).

Donc std :: move est juste utilisé comme précurseur de l'utilisation de la sémantique de déplacement. Déplacer la sémantique est essentiellement un moyen efficace de traiter des objets temporaires.

Considérez l'objet A = B + C + D + E + F;

C'est un joli code, mais E + F produit un objet temporaire. Ensuite, D + temp produit un autre objet temporaire et ainsi de suite. Dans chaque opérateur "+" normal d'une classe, des copies complètes se produisent.

Par exemple

Object Object::operator+ (const Object& rhs) {
    Object temp (*this);
    // logic for adding
    return temp;
}

La création de l'objet temporaire dans cette fonction est inutile - ces objets temporaires seront de toute façon supprimés à la fin de la ligne car ils seront hors de portée.

Nous pouvons plutôt utiliser la sémantique des mouvements pour "piller" les objets temporaires et faire quelque chose comme

 Object& Object::operator+ (Object&& rhs) {
     // logic to modify rhs directly
     return rhs;
 }

Cela évite de faire des copies profondes inutiles. En référence à l'exemple, la seule partie où la copie en profondeur se produit est maintenant E + F. Le reste utilise la sémantique de déplacement. Le constructeur de déplacement ou l'opérateur d'affectation doit également être implémenté pour affecter le résultat à A.

user929404
la source
3
vous avez parlé de la sémantique des mouvements. vous devez ajouter à votre réponse comment std :: move peut être utilisé parce que la question le demande.
Koushik Shetty
2
@Koushik std :: move ne fait pas grand chose - mais est utilisé pour implémenter la sémantique des mouvements. Si vous ne connaissez pas std :: move, vous ne connaissez probablement pas non plus la sémantique des mouvements
user929404
1
"ne fait pas grand chose" (oui juste un static_cast vers une référence rvalue). qu'est-ce que cela fait et y est ce que le PO a demandé. vous n'avez pas besoin de savoir comment fonctionne std :: move mais vous devez savoir ce que fait la sémantique des mouvements. en outre, "mais est utilisé pour implémenter la sémantique des mouvements" dans l'autre sens. connaître la sémantique des mouvements et vous comprendrez std :: move sinon non. move aide simplement au mouvement et utilise lui-même la sémantique des mouvements. std :: move ne fait que convertir son argument en référence rvalue, ce qui est nécessaire à la sémantique de move.
Koushik Shetty
10
"mais E + F produit un objet temporaire" - L'opérateur +va de gauche à droite, pas de droite à gauche. Ce B+Cserait donc le premier!
Ajay
8

"Qu'Est-ce que c'est?" et "Que fait-il?" a été expliqué ci-dessus.

Je vais donner un exemple de "quand il doit être utilisé".

Par exemple, nous avons une classe avec beaucoup de ressources comme un grand tableau.

class ResHeavy{ //  ResHeavy means heavy resource
    public:
        ResHeavy(int len=10):_upInt(new int[len]),_len(len){
            cout<<"default ctor"<<endl;
        }

        ResHeavy(const ResHeavy& rhs):_upInt(new int[rhs._len]),_len(rhs._len){
            cout<<"copy ctor"<<endl;
        }

        ResHeavy& operator=(const ResHeavy& rhs){
            _upInt.reset(new int[rhs._len]);
            _len = rhs._len;
            cout<<"operator= ctor"<<endl;
        }

        ResHeavy(ResHeavy&& rhs){
            _upInt = std::move(rhs._upInt);
            _len = rhs._len;
            rhs._len = 0;
            cout<<"move ctor"<<endl;
        }

    // check array valid
    bool is_up_valid(){
        return _upInt != nullptr;
    }

    private:
        std::unique_ptr<int[]> _upInt; // heavy array resource
        int _len; // length of int array
};

Code de test:

void test_std_move2(){
    ResHeavy rh; // only one int[]
    // operator rh

    // after some operator of rh, it becomes no-use
    // transform it to other object
    ResHeavy rh2 = std::move(rh); // rh becomes invalid

    // show rh, rh2 it valid
    if(rh.is_up_valid())
        cout<<"rh valid"<<endl;
    else
        cout<<"rh invalid"<<endl;

    if(rh2.is_up_valid())
        cout<<"rh2 valid"<<endl;
    else
        cout<<"rh2 invalid"<<endl;

    // new ResHeavy object, created by copy ctor
    ResHeavy rh3(rh2);  // two copy of int[]

    if(rh3.is_up_valid())
        cout<<"rh3 valid"<<endl;
    else
        cout<<"rh3 invalid"<<endl;
}

sortie comme ci-dessous:

default ctor
move ctor
rh invalid
rh2 valid
copy ctor
rh3 valid

Nous pouvons voir std::movequ'avec move constructorrend la ressource de transformation facilement.

Où d'autre est std::moveutile?

std::movepeut également être utile lors du tri d'un tableau d'éléments. De nombreux algorithmes de tri (tels que le tri par sélection et le tri par bulles) fonctionnent en échangeant des paires d'éléments. Auparavant, nous avons dû recourir à la sémantique de copie pour effectuer l'échange. Nous pouvons maintenant utiliser la sémantique de déplacement, qui est plus efficace.

Cela peut également être utile si nous voulons déplacer le contenu géré par un pointeur intelligent vers un autre.

Cité:

Jayhello
la source
0

Voici un exemple complet, en utilisant std :: move pour un vecteur personnalisé (simple)

Production attendue:

 c: [10][11]
 copy ctor called
 copy of c: [10][11]
 move ctor called
 moved c: [10][11]

Compiler en tant que:

  g++ -std=c++2a -O2 -Wall -pedantic foo.cpp

Code:

#include <iostream>
#include <algorithm>

template<class T> class MyVector {
private:
    T *data;
    size_t maxlen;
    size_t currlen;
public:
    MyVector<T> () : data (nullptr), maxlen(0), currlen(0) { }
    MyVector<T> (int maxlen) : data (new T [maxlen]), maxlen(maxlen), currlen(0) { }

    MyVector<T> (const MyVector& o) {
        std::cout << "copy ctor called" << std::endl;
        data = new T [o.maxlen];
        maxlen = o.maxlen;
        currlen = o.currlen;
        std::copy(o.data, o.data + o.maxlen, data);
    }

    MyVector<T> (const MyVector<T>&& o) {
        std::cout << "move ctor called" << std::endl;
        data = o.data;
        maxlen = o.maxlen;
        currlen = o.currlen;
    }

    void push_back (const T& i) {
        if (currlen >= maxlen) {
            maxlen *= 2;
            auto newdata = new T [maxlen];
            std::copy(data, data + currlen, newdata);
            if (data) {
                delete[] data;
            }
            data = newdata;
        }
        data[currlen++] = i;
    }

    friend std::ostream& operator<<(std::ostream &os, const MyVector<T>& o) {
        auto s = o.data;
        auto e = o.data + o.currlen;;
        while (s < e) {
            os << "[" << *s << "]";
            s++;
        }
        return os;
    }
};

int main() {
    auto c = new MyVector<int>(1);
    c->push_back(10);
    c->push_back(11);
    std::cout << "c: " << *c << std::endl;
    auto d = *c;
    std::cout << "copy of c: " << d << std::endl;
    auto e = std::move(*c);
    delete c;
    std::cout << "moved c: " << e << std::endl;
}
Neil McGill
la source