Quelles sont sémantique de déplacement?

je viens de finir d'écouter la radio de Software Engineering podcast interview with Scott Meyers regarding C++0x . La plupart des nouvelles fonctionnalités avaient du sens pour moi, et je suis actuellement excité par C++0x maintenant, à l'exception d'un. Je ne comprends toujours pas le sémantique de déplacement ... Quels sont-ils exactement?

1424
demandé sur RJFalconer 2010-06-24 02:46:46
la source

11 ответов

je trouve qu'il est plus facile de comprendre la sémantique des mouvements avec le code d'exemple. Commençons par une classe de chaîne très simple qui ne contient qu'un pointeur vers un bloc de mémoire alloué en tas:

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

    string(const char* p)
    {
        size_t size = strlen(p) + 1;
        data = new char[size];
        memcpy(data, p, size);
    }

puisque nous avons choisi de gérer la mémoire nous-mêmes, nous devons suivre la règle des trois . Je vais reporter l'écriture de l'opérateur de tâche et ne mettre en œuvre que le destructeur et le constructeur de copie pour le moment:

    ~string()
    {
        delete[] data;
    }

    string(const string& that)
    {
        size_t size = strlen(that.data) + 1;
        data = new char[size];
        memcpy(data, that.data, size);
    }

le constructeur de copies définit ce que signifie copier des objets string. Le paramètre const string& that se lie à toutes les expressions de la chaîne de caractères qui vous permet de faire des copies dans les exemples suivants:

string a(x);                                    // Line 1
string b(x + y);                                // Line 2
string c(some_function_returning_a_string());   // Line 3

vient maintenant la clé de compréhension de la sémantique des mouvements. Notez que seulement dans la première ligne où nous copions x est cette copie profonde vraiment nécessaire, parce que nous pourrions vouloir inspecter x plus tard et serait très surpris si x avait changé en quelque sorte. Avez-vous remarqué que je viens de dire x trois fois (quatre fois si vous incluez cette phrase) et que je voulais dire exactement le même objet à chaque fois? Nous appelons des expressions telles que x "lvalues".

les arguments dans les lignes 2 et 3 ne sont pas des valeurs L, mais des valeurs R, parce que les objets string sous-jacents n'ont pas de noms, de sorte que le client n'a aucun moyen de les inspecter à nouveau à un moment ultérieur. rvalues dénote des objets temporaires qui sont détruits au prochain point-virgule (pour être plus précis: à la fin de la pleine expression qui contient lexicalement la valeur R). C'est important car lors de l'initialisation de b et c , nous pouvions faire tout ce que nous voulions avec la chaîne source, et le client ne pouvait pas faire de différence !

C++0x introduit un nouveau mécanisme appelé "rvalue reference" qui, entre autres choses, permet de détecter les arguments de valeur via la surcharge de la fonction. Tout ce que nous avons à faire est d'écrire un constructeur avec un paramètre de référence rvalue. À l'intérieur de ce constructeur, nous pouvons faire tout ce que nous voulons avec la source, tant que nous le laissons dans quelque état valide:

    string(string&& that)   // string&& is an rvalue reference to a string
    {
        data = that.data;
        that.data = nullptr;
    }

Qu'avons-nous fait ici? Au lieu de copier en profondeur les données de tas, nous avons simplement copié le pointeur et réglé le pointeur original à null. En effet, nous avons "volé" les données il appartenait à l'origine à la chaîne source. Encore une fois, le point de vue clé est que le client ne pouvait en aucun cas détecter que la source avait été modifiée. Puisque nous ne faisons pas vraiment une copie ici, nous appelons ce constructeur un "move constructor". Son travail consiste à déplacer des ressources d'un objet à un autre au lieu de les copier.

félicitations, vous comprenez maintenant les bases de la sémantique des mouvements! Continuons en mettant en place l'opérateur d'affectation. Si vous n'êtes pas familier avec le copier et échanger idiom , apprendre et revenir, parce que c'est un impressionnant c++ idiom lié à la sécurité d'exception.

    string& operator=(string that)
    {
        std::swap(data, that.data);
        return *this;
    }
};

C'est ça? "Où est la référence rvalue?"vous pourriez demander. "Nous n'avons pas besoin ici!"est ma réponse :)

notez que nous passons le paramètre that en valeur , donc that doit être initialisé comme n'importe quel autre objet string. Exactement comment est that va être initialisé? Dans les vieux jours de C++98 , la réponse aurait été "par le constructeur de copie". Dans C++0x, le compilateur choisit entre le constructeur de copie et le constructeur de déplacement selon que l'argument de l'opérateur d'affectation est une lvalue ou une rvalue.

donc si vous dites a = b , le copy constructor va initialiser that (parce que l'expression b est un lvalue), et l'opérateur de cession échange le contenu avec une copie fraîchement créée, profonde. C'est la définition même de la copie et de swap idiome, faites une copie, d'échanger le contenu de la copie, puis se débarrasser de la copie en laissant le champ d'application. Rien de nouveau ici.

mais si vous dites a = x + y , le move constructor va initialiser that (parce que l'expression x + y est une valeur de R), donc il n'y a pas de copie profonde impliquée, seulement un geste efficace. that est encore un objet indépendant de l'argument, mais sa construction était triviale, puisque les données de tas n'ont pas dû être copiées, juste déplacées. Il n'était pas nécessaire de le copier car x + y est une valeur de R, et encore une fois, il est correct de se déplacer à partir d'objets string désignés par rvalues.

pour résumer, le constructeur de copie fait une copie profonde, parce que la source doit rester intacte. Le constructeur move, d'un autre côté, peut simplement copier le pointeur et ensuite définir le pointeur dans la source à null. Il est normal de "neutraliser" l'objet source de cette manière, parce que le client n'a aucun moyen d'inspecter l'objet à nouveau.

j'espère que cet exemple a fait passer le point principal. Il y a beaucoup plus à évaluer les références et à déplacer la sémantique que j'ai intentionnellement laissé de côté pour la simplicité. Si vous voulez plus de détails s'il vous plaît voir ma réponse supplémentaire .

2100
répondu fredoverflow 2017-05-23 15:10:45
la source

ma première réponse a été une introduction extrêmement simplifiée à la sémantique des mouvements, et de nombreux détails ont été omis volontairement pour la simplifier. Cependant, il y a beaucoup plus à déplacer sémantique, et j'ai pensé qu'il était temps pour une deuxième réponse pour combler les lacunes. La première réponse est déjà assez ancienne, et il ne semblait pas juste de la remplacer simplement par un texte complètement différent. Je pense que c'est encore une bonne introduction. Mais si vous voulez creuser un peu plus, lisez la suite :)

Stephan T. Lavavej a pris le temps de fournir des commentaires précieux. Merci beaucoup, Stephan!

Introduction

Move semantics permet à un objet, sous certaines conditions, de s'approprier les ressources externes d'un autre objet. Ceci est important de deux façons:

  1. Tournant cher des copies à bas prix se déplace. Voir ma première réponse pour un exemple. Notez que si un objet fait ne gérant pas au moins une ressource externe (soit directement, soit indirectement à travers ses objets membres), la sémantique move n'offrira aucun avantage par rapport à la sémantique de copie. Dans ce cas, copier un objet et déplacer un objet signifie exactement la même chose:

    class cannot_benefit_from_move_semantics
    {
        int a;        // moving an int means copying an int
        float b;      // moving a float means copying a float
        double c;     // moving a double means copying a double
        char d[64];   // moving a char array means copying a char array
    
        // ...
    };
    
  2. la mise en Œuvre de coffre-fort "déplacer" les types, c'est, types pour lesquels la copie n'a pas de sens, mais le déplacement n'. Exemples: serrures, poignées de fichiers et pointeurs intelligents avec une sémantique de propriété unique. Note: cette réponse parle de std::auto_ptr , un modèle de bibliothèque standard déprécié C++98, qui a été remplacé par std::unique_ptr en C++11. Les programmeurs intermédiaires C++ sont probablement au moins quelque peu familiers avec std::auto_ptr , et à cause de la" sémantique des mouvements " qu'il affiche, il semble être un bon point de départ pour discuter de la sémantique des mouvements en C++11. YMMV.

Qu'est-ce qu'un mouvement?

le La bibliothèque standard C++98 offre un pointeur intelligent avec une sémantique de propriété unique appelée std::auto_ptr<T> . Dans le cas où vous n'êtes pas familier avec auto_ptr , son but est de garantir qu'un objet alloué dynamiquement est toujours libéré, même en présence d'exceptions:

{
    std::auto_ptr<Shape> a(new Triangle);
    // ...
    // arbitrary code, could throw exceptions
    // ...
}   // <--- when a goes out of scope, the triangle is deleted automatically

la chose inhabituelle à propos de auto_ptr est son comportement de" copie":

auto_ptr<Shape> a(new Triangle);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        |
        |
  +-----|---+
  |   +-|-+ |
a | p | | | |
  |   +---+ |
  +---------+

auto_ptr<Shape> b(a);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        +----------------------+
                               |
  +---------+            +-----|---+
  |   +---+ |            |   +-|-+ |
a | p |   | |          b | p | | | |
  |   +---+ |            |   +---+ |
  +---------+            +---------+

noter comment l'initialisation de b avec a fait pas copier le triangle, mais au lieu de cela transfère la propriété du triangle de a à b . Nous disons aussi " a est emménagé dans b " ou "le triangle est déplacé à partir de a à b ". Cela peut sembler confus, parce que le triangle lui-même reste toujours au même endroit dans la mémoire.

déplacer un objet signifie transférer la propriété d'une ressource qu'il gère à un autre objet.

le constructeur de copies de auto_ptr ressemble probablement à quelque chose comme ceci (quelque peu simplifié):

auto_ptr(auto_ptr& source)   // note the missing const
{
    p = source.p;
    source.p = 0;   // now the source no longer owns the object
}

mouvements dangereux et inoffensifs

le danger de auto_ptr est que ce qui ressemble syntaxiquement à une copie est en fait un mouvement. Essayez d'appeler une fonction membre sur un déplacé-de auto_ptr va invoquer un comportement non défini, donc vous devez être très prudent de ne pas utiliser un auto_ptr après qu'il a été déplacé de:

auto_ptr<Shape> a(new Triangle);   // create triangle
auto_ptr<Shape> b(a);              // move a into b
double area = a->area();           // undefined behavior

mais auto_ptr n'est pas toujours dangereux. Les fonctions d'usine sont un cas d'utilisation parfaitement fin pour auto_ptr :

auto_ptr<Shape> make_triangle()
{
    return auto_ptr<Shape>(new Triangle);
}

auto_ptr<Shape> c(make_triangle());      // move temporary into c
double area = make_triangle()->area();   // perfectly safe

notez comment les deux exemples suivent le même schéma syntaxique:

auto_ptr<Shape> variable(expression);
double area = expression->area();

et pourtant, l'un d'eux appelle un comportement indéfini, alors que l'autre ne l'est pas. Quelle est donc la différence entre les expressions a et make_triangle() ? Ne sont-ils pas tous deux du même type? En effet, ils sont, mais ils ont différentes catégories de valeur .

catégories de valeur

évidemment, il doit y avoir une différence profonde entre l'expression a , qui dénote une variable auto_ptr , et la expression make_triangle() qui dénote l'appel d'une fonction qui renvoie un auto_ptr en valeur, créant ainsi un nouvel objet temporaire auto_ptr chaque fois qu'il est appelé. a est un exemple de valeur l , tandis que make_triangle() est un exemple de valeur R .

se déplacer à partir de valeurs L comme a est dangereux, parce que nous pourrions plus tard essayer d'appeler une fonction membre via a , invoquer un comportement indéfini. D'un autre côté, se déplacer à partir de valeurs telles que make_triangle() est parfaitement sûr, car après que le constructeur de copie a fait son travail, nous ne pouvons plus utiliser le temporaire. Il n'y a pas d'expression qui dénote dit temporaire; si nous écrivons simplement make_triangle() encore une fois, nous obtenons un différent temporaire. En fait, le déplacé-de temporaire est déjà parti sur la ligne suivante:

auto_ptr<Shape> c(make_triangle());
                                  ^ the moved-from temporary dies right here

Notez que les lettres l et r ont une origine historique dans le côté gauche et le côté droit d'une affectation. Ce n'est plus le cas en C++, car il y a des valeurs L qui ne peuvent pas apparaître à gauche d'une tâche (comme des tableaux ou des types définis par l'utilisateur sans opérateur de tâche), et il y a des valeurs R qui le peuvent (toutes les valeurs des types de classe avec opérateur de tâche).

une valeur de type de classe est une expression dont l'évaluation crée un objet temporaire. Dans des circonstances normales, aucune autre expression à l'intérieur d'une même portée n'indique le même objet temporaire.

références Rvalue

Nous comprenons maintenant que le passage de lvalues est potentiellement dangereux, mais passer de rvalues est inoffensif. Si C++ avait un support de langage pour distinguer les arguments lvalue des arguments rvalue, nous pourrions soit interdire complètement le déplacement de lvalues, soit au moins faire bouger de lvalues explicite sur le site d'appel, de sorte que nous ne nous déplaçons plus par accident.

C++11, la réponse à ce problème est références rvalue . Référence rvalue est un nouveau type de référence qui se lie uniquement à rvalues, et la syntaxe est X&& . La bonne vieille référence X& est maintenant connue comme une référence lvalue . (Notez que X&& est et non a référence à une référence; il n'y a pas de telle chose en C++.)

si nous jetons const dans le mix, nous avons déjà quatre types différents de références. À quels types d'expressions de type X peuvent-ils se rattacher?

            lvalue   const lvalue   rvalue   const rvalue
---------------------------------------------------------              
X&          yes
const X&    yes      yes            yes      yes
X&&                                 yes
const X&&                           yes      yes

Dans la pratique, vous pouvez oublier const X&& . Être limité à lire à partir de valeurs R n'est pas très utile.

référence rvalue X&& est une nouvelle une sorte de référence qui ne se lie qu'aux valeurs de R.

conversions implicites

références Rvalue a connu plusieurs versions. Depuis la version 2.1 , une référence de valeur X&& se lie également à toutes les catégories de valeur d'un type différent Y , à condition qu'il y ait une conversion implicite de Y en X . Dans ce cas, un temporaire de type X est créé, et la référence rvalue est lié à que temporaire:

void some_function(std::string&& r);

some_function("hello world");

dans l'exemple ci-dessus, "hello world" est une valeur l de type const char[12] . Puisqu'il y a une conversion implicite de const char[12] à const char* en std::string , un temporaire de type std::string est créé, et r est lié à ce temporaire. C'est l'un des cas où la distinction entre les valeurs (expressions) et les temporalités (objets) est un peu floue.

constructeurs de Déplacement

un exemple utile d'une fonction avec un paramètre X&& est le move constructor X::X(X&& source) . Son but est de transférer la propriété de la ressource gérée par la source dans l'objet courant.

En C++11, std::auto_ptr<T> a été remplacé par std::unique_ptr<T> , qui tire parti des références rvalue. Je développerai et discuterai une version simplifiée de unique_ptr . Tout d'abord, nous encapsuler un raw pointer et surcharger les opérateurs -> et * , de sorte que notre classe se sent comme un pointeur:

template<typename T>
class unique_ptr
{
    T* ptr;

public:

    T* operator->() const
    {
        return ptr;
    }

    T& operator*() const
    {
        return *ptr;
    }

Le constructeur prend possession de l'objet, et le destructeur supprime:

    explicit unique_ptr(T* p = nullptr)
    {
        ptr = p;
    }

    ~unique_ptr()
    {
        delete ptr;
    }

vient maintenant la partie intéressante, le constructeur de mouvements:

    unique_ptr(unique_ptr&& source)   // note the rvalue reference
    {
        ptr = source.ptr;
        source.ptr = nullptr;
    }

ce constructeur de déménagement fait exactement ce que le constructeur de copie auto_ptr a fait, mais il ne peut être fourni avec rvalues:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);                 // error
unique_ptr<Shape> c(make_triangle());   // okay

la deuxième ligne ne compile pas, car a est une valeur l, mais le paramètre unique_ptr&& source ne peut être lié qu'aux valeurs R. C'est exactement ce que nous voulions; mouvements dangereux ne devrait jamais être implicite. La troisième ligne compile très bien, parce que make_triangle() est une valeur. Le constructeur de déménagement transférera la propriété du temporaire au c . Encore une fois, c'est exactement ce que nous voulions.

le constructeur de déménagement transfère la propriété d'une ressource gérée dans l'objet courant.

Déplacer les opérateurs d'affectation

la dernière pièce manquante est l'opérateur de déménagement. Son travail est de libérer l'ancienne ressource et d'acquérir la nouvelle ressource de son argument:

    unique_ptr& operator=(unique_ptr&& source)   // note the rvalue reference
    {
        if (this != &source)    // beware of self-assignment
        {
            delete ptr;         // release the old resource

            ptr = source.ptr;   // acquire the new resource
            source.ptr = nullptr;
        }
        return *this;
    }
};

notez comment cette mise en œuvre de l'opérateur d'assignation de déplacement duplique la logique des deux destructeurs et le constructeur de déplacement. Connaissez-vous l'idiome du copy-and-swap? Il peut également être appliqué à la sémantique des mouvements comme l'idiome du mouvement et de l'échange:

    unique_ptr& operator=(unique_ptr source)   // note the missing reference
    {
        std::swap(ptr, source.ptr);
        return *this;
    }
};

Maintenant que source est une variable de type unique_ptr , il sera initialisé par le constructeur de déplacement; c'est, l'argument va être déplacé dans le paramètre. L'argument doit toujours être rvalue, car le constructeur de mouvements lui-même a un paramètre de référence rvalue. Lorsque le débit de commande atteint le corset de fermeture de operator= , source sort de la portée, libérant l'ancienne ressource automatiquement.

l'opérateur move assignment transfère la propriété d'une ressource gérée dans l'objet courant, libérant l'ancienne ressource. Le langage move-and-swap simplifie la mise en œuvre.

passer de l'lvalues

parfois, nous voulons nous éloigner de lvalues. C'est, parfois, nous voulons le compilateur de traiter une lvalue, comme si c'était une rvalue, de sorte qu'il peut invoquer le constructeur de déplacement, même si elle pourrait être potentiellement dangereux. À cet effet, C++11 propose un modèle de fonction de bibliothèque standard appelé std::move dans l'en-tête <utility> . Ce nom est un peu malheureux, parce que std::move donne simplement une valeur de l à une valeur de r; il fait pas déplacer n'importe quoi par lui-même. Il suffit de permet déménagement. Peut-être qu'il aurait été nommé std::cast_to_rvalue ou std::enable_move , mais nous sommes coincés avec le nom maintenant.

Voici comment vous vous déplacez explicitement d'une valeur l:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);              // still an error
unique_ptr<Shape> c(std::move(a));   // okay

notez qu'après la troisième ligne, a ne possède plus de triangle. C'est d'accord, parce que par explicitement écriture std::move(a) , nous avons fait clairement nos intentions: "cher constructeur, faire ce que vous voulez avec a en ordre d'initialiser c ; Je ne me soucie plus de a . N'hésitez pas à faire ce que vous voulez avec a ."

std::move(some_lvalue) projette une valeur l à une valeur R, permettant ainsi un déplacement ultérieur.

Xvalues

notez que même si std::move(a) est une valeur, son évaluation ne pas crée un objet temporaire. Cette énigme forcé le Comité introduira une troisième catégorie de valeurs. Quelque chose qui peut être lié à une référence de valeur, même s'il ne s'agit pas d'une valeur au sens traditionnel, est appelé une xvalue (valeur qui expire). Les valeurs R traditionnelles ont été renommées en prvalues (valeurs R pures).

les valeurs PR et X sont des valeurs R. Xvalues et lvalues sont toutes deux glvalues (lvalues généralisées). Le les relations sont plus faciles à saisir avec un diagramme:

        expressions
          /     \
         /       \
        /         \
    glvalues   rvalues
      /  \       /  \
     /    \     /    \
    /      \   /      \
lvalues   xvalues   prvalues

notez que seules les valeurs X sont vraiment nouvelles; le reste est simplement dû au changement de nom et au regroupement.

C++98 rvalues sont connus comme prvalues en C++11. Remplacer mentalement toutes les occurrences de "valeur" dans les paragraphes précédents par "valeur".

déplacement hors fonction

jusqu'à présent, nous avons vu le mouvement dans les variables locales, et dans les paramètres de fonction. Mais se déplacer est aussi possible dans la direction opposée. Si une fonction retourne en valeur, un objet au site d'appel (probablement une variable locale ou une variable temporaire, mais qui pourrait être n'importe quel objet) est initialisé avec l'expression après l'instruction return comme argument au constructeur move:

unique_ptr<Shape> make_triangle()
{
    return unique_ptr<Shape>(new Triangle);
}          \-----------------------------/
                  |
                  | temporary is moved into c
                  |
                  v
unique_ptr<Shape> c(make_triangle());

peut-être étonnamment, objets automatiques (variables locales qui ne sont pas déclarées comme static ) peut aussi être implicitement déplacé hors des fonctions:

unique_ptr<Shape> make_square()
{
    unique_ptr<Shape> result(new Square);
    return result;   // note the missing std::move
}

comment se fait-il que le constructeur de mouvements accepte la valeur result comme argument? La portée de result est sur le point de prendre fin, et elle sera détruite pendant le déroulement de la pile. Personne ne pourrait se plaindre par la suite que result a changé d'une façon ou d'une autre; quand le flux de contrôle est de retour à l'appelant, result n'existe plus! Pour cette raison, C++11 a un règle spéciale qui permet de retourner des objets automatiques à partir de fonctions sans avoir à écrire std::move . En fait, vous devriez jamais utiliser std::move pour déplacer les objets automatiques hors des fonctions, car cela inhibe l ' "optimisation de la valeur de retour nommée" (NRVO).

ne Jamais utiliser de std::move pour déplacer automatique des objets des fonctions.

noter que dans les deux fonctions d'usine, le le type de retour est une valeur, pas une référence rvalue. Les références de valeur sont toujours des références, et comme toujours, vous ne devez jamais retourner une référence à un objet automatique; l'appelant se retrouverait avec une référence pendante si vous avez piégé le compilateur pour qu'il accepte votre code, comme ceci:

unique_ptr<Shape>&& flawed_attempt()   // DO NOT DO THIS!
{
    unique_ptr<Shape> very_bad_idea(new Square);
    return std::move(very_bad_idea);   // WRONG!
}

Jamais de retour automatique d'objets par référence rvalue. Le déplacement est exclusivement effectué par le constructeur de déplacement, et non par std::move , et non par liant une valeur à une référence de valeur.

dans les membres

Tôt ou tard, vous allez écrire le code comme ceci:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(parameter)   // error
    {}
};

fondamentalement, le compilateur se plaindra que parameter est une valeur de l. Si vous regardez son type, vous verrez une référence rvalue, mais une référence rvalue signifie simplement "une référence qui est lié à une rvalue"; il ne pas signifie que la référence elle-même est une valeur! En effet, parameter est juste une variable ordinaire avec un nom. Vous pouvez utiliser parameter aussi souvent que vous le souhaitez dans le corps du constructeur, et il indique toujours le même objet. S'en écarter implicitement serait dangereux, d'où son interdiction.

une référence de valeur nommée est une valeur de l, comme n'importe quelle autre variable.

la solution est de activer manuellement le déplacement:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(std::move(parameter))   // note the std::move
    {}
};

on pourrait dire que parameter n'est plus utilisé après l'initialisation de member . Pourquoi n'y a-t-il pas de règle spéciale pour insérer silencieusement std::move comme pour les valeurs de retour? Probablement parce que cela serait trop lourd pour les réalisateurs du compilateur. Par exemple, si le corps du constructeur était dans une autre unité de traduction? En revanche, la règle de valeur de retour doit simplement vérifier les tables de symboles pour déterminez si l'identificateur qui suit le mot-clé return dénote ou non un objet automatique.

vous pouvez également passer parameter en valeur. Pour les types comme unique_ptr , il semble qu'il n'y ait pas encore d'idiome établi. Personnellement, je préfère passer par valeur, car il provoque moins d'encombrement dans l'interface.

fonctions spéciales des membres

c++98 déclare implicitement trois fonctions spéciales de membre sur demande, c'est-à-dire quand ils sont nécessaires quelque part: le constructeur de copie, l'opérateur de copie et le destructeur.

X::X(const X&);              // copy constructor
X& X::operator=(const X&);   // copy assignment operator
X::~X();                     // destructor

références Rvalue a connu plusieurs versions. Depuis la version 3.0, C++11 déclare deux fonctions supplémentaires de membre spécial sur demande: le constructeur de déménagement et l'opérateur d'affectation de déménagement. Notez que ni VC10 ni VC11 ne sont encore conformes à la version 3.0, vous devrez donc les implémenter vous-même.

X::X(X&&);                   // move constructor
X& X::operator=(X&&);        // move assignment operator

ces deux nouvelles fonctions de membre spécial ne sont déclarées que implicitement si aucune des fonctions de membre spécial n'est déclarée manuellement. En outre, si vous déclarez votre propre constructeur de déménagement ou opérateur d'affectation de déménagement, ni le constructeur de copie ni l'opérateur d'affectation de copie ne seront déclarés implicitement.

que signifient ces règles dans la pratique?

Si vous écrivez une classe sans ressources non managées, il n'est pas nécessaire pour déclarer l'une des cinq fonctions de membre spécial vous-même, et vous obtiendrez la sémantique de copie correcte et déplacer la sémantique gratuitement. Sinon, vous devrez mettre en œuvre vous-même les fonctions spéciales de membre. Bien sûr, si votre classe ne bénéficie pas de la sémantique de déplacement, il n'est pas nécessaire de mettre en œuvre les opérations de déplacement spécial.

noter que l'opérateur copy assignment et l'opérateur move assignment peuvent être fusionnés en une seule tâche unifiée exploitant, en prenant son argument par valeur:

X& X::operator=(X source)    // unified assignment operator
{
    swap(source);            // see my first answer for an explanation
    return *this;
}

de cette façon, le nombre de fonctions spéciales de membre à mettre en œuvre passe de cinq à quatre. Il y a ici un compromis entre exception-sécurité et efficacité, mais je ne suis pas un expert en la matière.

Forwarding references ( antérieurement connu sous le nom de Universal references )

considère la fonction suivante modèle:

template<typename T>
void foo(T&&);

vous pourriez vous attendre à ce que T&& ne se lie qu'aux valeurs de R, parce qu'à première vue, cela ressemble à une référence de rvalue. Comme il s'avère cependant, T&& se lie également à lvalues:

foo(make_triangle());   // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a);                 // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&

si l'argument est une valeur de type X , T est déduit pour être X , donc T&& signifie X&& . C'est ce que l'on pourrait en attendre. Mais si l'argument est une valeur de type X , en raison d'une règle spéciale, T est déduite pour être X& , donc T&& signifierait quelque chose comme X& && . Mais comme C++ n'a toujours pas de notion de références à des références, le type X& && est s'est effondré en X& . Cela peut sembler confus et inutile au début, mais l'effondrement de référence est essentielle pour perfect forwarding (qui ne sera pas discuté ici).

T&& n'est pas une référence rvalue, mais un transfert de référence. Il se lie également à lvalues, auquel cas T et T&& sont tous deux des références de lvalue.

si vous voulez contraindre un modèle de fonction à des valeurs R, Vous pouvez combiner SFINAE avec des caractères de type:

#include <type_traits>

template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);

mise en œuvre du déménagement

maintenant que vous comprenez référence s'effondrant, voici comment std::move est mis en œuvre:

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

comme vous pouvez le voir, move accepte n'importe quel type de paramètre grâce à la référence d'expédition T&& , et il renvoie une référence de valeur. L'appel de méta-fonction std::remove_reference<T>::type est nécessaire car sinon, pour les valeurs L de type X , le type de retour serait X& && , qui s'effondrerait en X& . Depuis t est toujours une valeur l (rappelez-vous qu'une référence rvalue nommée est une valeur l), mais nous voulons lier t à une référence rvalue, nous devons lancer explicitement t au bon type de retour. L'appel d'une fonction qui retourne une référence rvalue est lui-même un xvalue. Maintenant, vous savez où xvalues ;)

L'appel d'une fonction qui retourne une référence rvalue, tels que std::move , est une value.

notez que le renvoi par référence à la valeur est très bien dans cet exemple, parce que t ne signifie pas un objet automatique, mais plutôt un objet qui a été transmis par l'appelant.

911
répondu fredoverflow 2018-06-22 00:33:38
la source

sémantique de Déplacement sont basées sur des références rvalue .

Une rvalue est un objet temporaire, qui va être détruit à la fin de l'expression. Dans le C++ actuel, les valeurs de R ne se lient qu'aux références const . C++1x va permettre des références rvalue const , orthographiées T&& , qui sont des références à des objets rvalue.

Depuis une rvalue va mourir à la fin de une expression, vous pouvez voler ses données . Au lieu de copie dans un autre objet, vous déplacer ses données.

class X {
public: 
  X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
    : data_()
  {
     // since 'x' is an rvalue object, we can steal its data
     this->swap(std::move(rhs));
     // this will leave rhs with the empty data
  }
  void swap(X&& rhs);
  // ... 
};

// ...

X f();

X x = f(); // f() returns result as rvalue, so this calls move-ctor

dans le code ci-dessus, avec les anciens compilateurs le résultat de f() est copié dans x en utilisant X 's copy constructor. Si votre compilateur supporte la sémantique des mouvements et X a un déplacez-constructeur, puis qui est appelée à la place. Puisque son argument rhs est un rvalue , nous savons qu'il n'est plus nécessaire et nous pouvons voler sa valeur.

Donc la valeur est déplacé du temporaire sans nom retourné de f() à x (tandis que les données de x , initialisé à un vide X , est déplacé dans le temporaire, qui sera détruit après affectation.)

72
répondu sbi 2017-11-24 18:23:54
la source

supposons que vous ayez une fonction qui renvoie un objet substantiel:

Matrix multiply(const Matrix &a, const Matrix &b);

quand vous écrivez un code comme ceci:

Matrix r = multiply(a, b);

alors un compilateur C++ ordinaire créera un objet temporaire pour le résultat de multiply() , appellera le constructeur de copie pour initialiser r , puis détruira la valeur de retour temporaire. La sémantique des mouvements en C++0x permet au "constructeur de mouvements" d'être appelé pour initialiser r en copiant son le contenu, puis jeter la valeur temporaire sans avoir à la détruire.

ceci est particulièrement important si (comme peut-être l'exemple Matrix ci-dessus), l'objet copié alloue de la mémoire supplémentaire sur le tas pour stocker sa représentation interne. Un constructeur de copie devrait soit faire une copie complète de la représentation interne, soit utiliser le comptage de référence et la sémantique de copie-sur-écriture interalement. Un constructeur de mouvements laisserait la mémoire de tas seule et juste Copiez le pointeur à l'intérieur de l'objet Matrix .

52
répondu Greg Hewgill 2010-06-24 02:53:31
la source

si vous êtes vraiment intéressé par une bonne explication détaillée de la sémantique des mouvements, je vous recommande fortement de lire le papier original sur eux, " une proposition pour ajouter le Support de la sémantique des mouvements au langage C++."

Il est très accessible et facile à lire et c'est une excellente affaire pour les avantages qu'ils offrent. Il existe d'autres documents plus récents et à jour sur la sémantique des mouvements disponibles sur le site Web du GT21 , mais celui-ci est probablement le plus simple car il aborde les choses d'un point de vue de haut niveau et n'obtient pas beaucoup dans les détails de langue graveleuse.

29
répondu James McNellis 2010-06-24 03:32:29
la source

Move semantics est d'environ transférer des ressources plutôt que de les copier quand personne n'a plus besoin de la valeur source.

dans C++03, Les objets sont souvent copiés, seulement pour être détruits ou assignés-over avant qu'un code utilise de nouveau la valeur. Par exemple, lorsque vous retournez par valeur à partir d'une fonction-à moins que RVO ne démarre-la valeur que vous retournez est copiée dans le cadre de la pile de l'appelant, puis elle sort de la portée et est détruit. Ce n'est qu'un exemple parmi tant d'autres: voir pass-by-value quand l'objet source est temporaire, des algorithmes comme sort qui ne font que réarranger des éléments, réallocation dans vector quand son capacity() est dépassé, etc.

quand de telles paires de copie / destruction sont coûteuses, c'est typiquement parce que l'objet possède une certaine ressource poids lourd. Par exemple, vector<string> peut posséder un bloc mémoire attribué dynamiquement contenant un tableau d'objets string , chaque avec sa propre mémoire dynamique. Copier un tel objet est coûteux: vous devez allouer une nouvelle mémoire pour chaque bloc alloué dynamiquement dans la source, et copier toutes les valeurs à travers. puis vous devez désallouer toute la mémoire que vous venez de copier. Cependant, se déplaçant un grand vector<string> signifie simplement copier quelques pointeurs (qui se réfèrent au bloc de mémoire dynamique) à la destination et les mettre à zéro dans la source.

26
répondu Dave Abrahams 2012-04-08 23:47:56
la source

en termes faciles (pratiques):

copier un objet signifie copier ses éléments "statiques" et appeler l'opérateur new pour ses objets dynamiques. Droit?

class A
{
   int i, *p;

public:
   A(const A& a) : i(a.i), p(new int(*a.p)) {}
   ~A() { delete p; }
};

cependant, à move un objet (je répète, d'un point de vue pratique) implique seulement de copier les pointeurs des objets dynamiques, et non d'en créer de nouveaux.

Mais, n'est-ce pas dangereux? Bien sûr, vous pourriez détruire deux fois un objet dynamique (défaut de segmentation). Donc, pour éviter cela, vous devriez "invalider" les pointeurs de source pour éviter de les détruire deux fois:

class A
{
   int i, *p;

public:
   // Movement of an object inside a copy constructor.
   A(const A& a) : i(a.i), p(a.p)
   {
     a.p = nullptr; // pointer invalidated.
   }

   ~A() { delete p; }
   // Deleting NULL, 0 or nullptr (address 0x0) is safe. 
};

Ok, mais si je déplace un objet, l'objet source devient inutile, non? Bien sûr, mais dans certaines situations, c'est très utile. Le plus évident, c'est quand j'appelle une fonction avec un objet anonyme (temporelle, rvalue objet ..., vous pouvez l'appeler avec des noms différents):

void heavyFunction(HeavyType());

dans cette situation, un objet anonyme est créé, copié ensuite dans le paramètre Fonction, puis supprimé. Donc, ici, il est préférable de déplacer l'objet, parce que vous n'avez pas besoin de l'objet anonyme et vous pouvez gagner du temps et de la mémoire.

cela conduit à la notion de référence" rvalue". Ils n'existent en C++11 que pour détecter si l'objet reçu est anonyme ou non. Je pense que vous savez déjà qu'une "valeur l" est une entité assignable (la partie gauche de la = ), vous avez donc besoin d'une référence à un objet pour être capable d'agir comme une valeur l. Une valeur est exactement le contraire, un objet sans références nommées. Pour cette raison, objet anonyme et valeur sont synonymes. So:

class A
{
   int i, *p;

public:
   // Copy
   A(const A& a) : i(a.i), p(new int(*a.p)) {}

   // Movement (&& means "rvalue reference to")
   A(A&& a) : i(a.i), p(a.p)
   {
      a.p = nullptr;
   }

   ~A() { delete p; }
};

Dans ce cas, lorsqu'un objet de type A doit être "copié", le compilateur crée une lvalue de référence ou une référence rvalue selon si l'objet passé est nommé ou non. Lorsqu'il n'est pas, votre constructeur est appelé et vous savez que l'objet est temporel et vous pouvez déplacer ses objets dynamiques au lieu de les copier, en économisant de l'espace et de la mémoire.

il est important de se rappeler que les objets" statiques " sont toujours copiés. Il n'y a aucun moyen de "déplacer" un objet statique (objet empilé et non sur un tas). Ainsi, la distinction "déplacer"/ "copier" lorsqu'un objet n'a pas de membres dynamiques (directement ou indirectement) n'est pas pertinente.

si votre objet est complexe et que le destructeur a d'autres effets secondaires, comme appeler à la fonction d'une bibliothèque, appeler à d'autres fonctions globales ou quoi que ce soit d'autre, est peut-être mieux pour signaler un mouvement avec un drapeau:

class Heavy
{
   bool b_moved;
   // staff

public:
   A(const A& a) { /* definition */ }
   A(A&& a) : // initialization list
   {
      a.b_moved = true;
   }

   ~A() { if (!b_moved) /* destruct object */ }
};

ainsi, votre code est plus court (vous n'avez pas besoin de faire une assignation nullptr pour chaque membre dynamique) et plus général.

autre question typique: Quelle est la différence entre A&& et const A&& ? Bien sûr, dans le premier cas, vous pouvez modifier la objet et dans le second pas, mais, sens pratique? Dans le second cas, vous ne pouvez pas le modifier, donc vous n'avez aucun moyen d'invalider l'objet (sauf avec un drapeau mutable ou quelque chose comme ça), et il n'y a aucune différence pratique pour un constructeur de copie.

et perfect forwarding ? Il est important de savoir qu'une "référence rvalue" est une référence à un objet nommé dans le "visiteur du champ d'application". Mais dans le cadre réel, une référence de valeur est nom d'un objet, donc, il agit comme un objet nommé. Si vous passez une référence rvalue à une autre fonction, vous passez un objet nommé, donc, l'objet n'est pas reçu comme un objet temporel.

void some_function(A&& a)
{
   other_function(a);
}

l'objet a serait copié sur le paramètre réel de other_function . Si vous voulez que l'objet a continue d'être traité comme un objet temporaire, vous devez utiliser la fonction std::move :

other_function(std::move(a));

avec cette ligne, std::move lancera a à une valeur R et other_function recevra l'objet comme un objet sans nom. Bien sûr, si other_function n'a pas de surcharge spécifique pour fonctionner avec des objets sans nom, cette distinction n'est pas importante.

est-ce que c'est parfait? Non, mais nous sommes très proches. Perfect forwarding est seulement utile pour travailler avec des modèles, avec le but de dire: si j'ai besoin de passer un objet à une autre fonction, j'ai besoin que si je reçois un nommé d'objet, l'objet est passé comme un objet nommé, et lorsqu'il n'est pas, je tiens à le passer comme un objet sans nom:

template<typename T>
void some_function(T&& a)
{
   other_function(std::forward<T>(a));
}

c'est la signature d'une fonction prototypique qui utilise perfect forwarding, implémentée en C++11 au moyen de std::forward . Cette fonction exploite quelques règles d'instanciation de template:

 `A& && == A&`
 `A&& && == A&&`

donc, si T est une valeur de référence à A ( T = A&), a aussi ( & && =>&). Si T est une rvalue reference à A , a aussi (A&&&& =>&&). Dans les deux cas, a est un objet nommé dans la portée réelle, mais T contient les informations de son "type de référence" du point de vue de la portée de l'appelant. Cette information ( T ) est transmise comme paramètre de modèle à forward et" a "est déplacé ou non selon le type de T .

22
répondu Peregring-lk 2015-02-21 02:09:00
la source

C'est comme la sémantique de copie, mais au lieu d'avoir à dupliquer toutes les données que vous obtenez pour voler les données de l'objet "déplacé".

19
répondu Terry Mahaffey 2013-09-27 11:48:25
la source

vous savez ce que signifie une sémantique de copie? cela signifie que vous avez des types qui sont copiables, pour les types définis par l'utilisateur, vous définissez ceci soit en écrivant explicitement un constructeur de copie et un opérateur de tâche, soit le compilateur les génère implicitement. Cela va faire une copie.

déplacer sémantique est essentiellement un type défini par l'utilisateur avec un constructeur qui prend un r-valeur de référence (nouveau type de référence en utilisant & & (Oui deux ampersands)) qui est non-const, ce est appelé un mouvement constructeur, même chose pour l'opérateur d'affectation. Donc qu'est-ce qu'un constructeur de mouvements fait, au lieu de copier de la mémoire à partir de son argument source, il "déplace" la mémoire de la source à la destination.

quand voudriez-vous faire ça? well std:: vector est un exemple, disons que vous avez créé un std temporaire:: vector et que vous le renvoyez d'une fonction dites:

std::vector<foo> get_foos();

vous allez avoir les frais généraux du constructeur de copie quand la fonction retourne, si (et il va dans C++0x) std::vector a un constructeur de mouvements au lieu de copier Il peut simplement définir ses pointeurs et 'déplacer' dynamiquement la mémoire allouée à la nouvelle instance. C'est un peu comme la sémantique du transfert de propriété avec std::auto_ptr.

13
répondu snk_kid 2010-06-24 03:03:36
la source

pour illustrer la nécessité de déplacer sémantique , considérons cet exemple sans déplacer sémantique:

Voici une fonction qui prend un objet de type T et renvoie un objet de même type T :

T f(T o) { return o; }
  //^^^ new object constructed

la fonction ci-dessus utilise appel en valeur ce qui signifie que lorsque cette fonction est appelée un objet doit être construit pour être utilisé par la fonction.

Parce que la fonction aussi retourne en valeur , un autre nouvel objet est construit pour la valeur de retour:

T b = f(a);
  //^ new object constructed

deux de nouveaux objets ont été construits, dont l'un est un objet temporaire qui n'est utilisé que pour la durée de la fonction.

Lorsque le nouvel objet est créé à partir de la valeur de retour, le constructeur de copie est appelé à copier le contenu de l'objet temporaire dans le nouvel objet B. Une fois la fonction terminée, l'objet temporaire utilisé dans la fonction sort de sa portée et est détruit.


maintenant, considérons ce qu'un copy constructor fait.

il doit d'abord initialiser l'objet, puis copier toutes les données pertinentes de l'ancien objet vers le nouveau.

Selon la classe, peut-être son un conteneur avec beaucoup de données, alors qui pourrait représenter beaucoup temps et utilisation de la mémoire

// Copy constructor
T::T(T &old) {
    copy_data(m_a, old.m_a);
    copy_data(m_b, old.m_b);
    copy_data(m_c, old.m_c);
}

avec move semantics il est maintenant possible de rendre la plupart de ce travail moins désagréable par simplement déplacer les données plutôt que de copier.

// Move constructor
T::T(T &&old) noexcept {
    m_a = std::move(old.m_a);
    m_b = std::move(old.m_b);
    m_c = std::move(old.m_c);
}

le déplacement des données implique une nouvelle association des données avec le nouvel objet. Et aucune copie n'a lieu du tout.

ceci est accompli avec une référence rvalue .

Une référence rvalue fonctionne à peu près comme une référence lvalue avec une différence importante:

une référence de valeur peut être déplacée et une référence de valeur ne peut pas être déplacée.

de cppreference.com :

pour permettre une forte garantie d'exception, les constructeurs de déménagement définis par l'utilisateur ne devraient pas lancer d'exceptions. En fait, les conteneurs standards s'appuient généralement sur std::move_if_noexcept pour choisir entre déplacer et copier quand des éléments de conteneur doivent être déplacés. Si les deux constructeurs copy et move sont fournis, overload resolution sélectionne le constructeur move si l'argument est une valeur R (soit une valeur PR comme nameless temporaire ou une valeur de X telle que le résultat de std:: move), et sélectionne le constructeur de copie si l'argument est une valeur de l (objet nommé ou une fonction/opérateur retournant la référence de valeur de l). Si seul le constructeur de copie est fourni, toutes les catégories d'arguments le sélectionnent (aussi longtemps qu'il prend une référence à const, puisque rvalues peut se lier aux références const), ce qui rend la copie de secours pour le déplacement, quand le déplacement n'est pas disponible. Dans de nombreuses situations, les constructeurs de mouvements sont optimisés même s'ils produire des effets secondaires observables, voir la copie de décision. Un constructeur est appelé 'move constructor' lorsqu'il prend une référence rvalue comme paramètre. Il n'est pas obligé de déplacer quoi que ce soit, la classe n'est pas obligée d'avoir une ressource à déplacer et un "constructeur de déplacement" peut ne pas être capable de déplacer une ressource comme dans le cas autorisé (mais peut-être pas raisonnable) où le paramètre est une référence de valeur constante (const T&&).

7
répondu Andreas DM 2016-02-25 03:12:28
la source

j'écris ceci pour m'assurer de bien le comprendre.

La sémantique des mouvements

a été créée pour éviter la copie inutile de gros objets. Bjarne Stroustrup dans son livre "The C++ Programming Language" utilise deux exemples où la copie inutile se produit par défaut: un, l'échange de deux grands objets, et deux, le retour d'un grand objet à partir d'une méthode.

L'échange de deux grands objets implique habituellement la copie du premier objet à un objet temporaire, copier le second objet sur le premier objet, et copier l'objet temporaire sur le second objet. Pour un type intégré, c'est très rapide, mais pour de grands objets ces trois copies peuvent prendre beaucoup de temps. Une "assignation de déplacement" permet au programmeur d'Outrepasser le comportement de copie par défaut et à la place d'échanger des références aux objets, ce qui signifie qu'il n'y a pas de copie du tout et que l'opération de swap est beaucoup plus rapide. L'assignation de déplacement peut être invoquée en appelant le std::move() de la méthode.

retourner un objet à partir d'une méthode par défaut implique de faire une copie de l'objet local et de ses données associées dans un endroit qui est accessible à l'appelant (parce que l'objet local n'est pas accessible à l'appelant et disparaît lorsque la méthode est terminée). Quand un type est retourné, cette opération est très rapide, mais si un grand objet est retourné, cela pourrait prendre du temps. Le constructeur de déplacement permet au programmeur de outrepassez ce comportement par défaut et "réutilisez" les données tas associées à l'objet local en pointant l'objet retourné à l'appelant vers les données tas associées à l'objet local. Donc pas de copie est nécessaire.

Dans des langues qui ne permettent pas la création d'objets locaux (c'est-à-objets sur la pile) ces types de problèmes ne se produisent pas tous les objets sont alloués sur le tas et sont toujours accessibles par référence.

5
répondu Chris B 2016-11-22 08:23:14
la source

Autres questions sur c++ c++-faq c++11 move-semantics