136 votes

Déplacer l'opérateur d'affectation et `if (this != &rhs)`.

Dans l'opérateur d'assignation d'une classe, vous devez généralement vérifier si l'objet assigné est l'objet invoquant afin de ne pas tout gâcher :

Class& Class::operator=(const Class& rhs) {
    if (this != &rhs) {
        // do the assignment
    }

    return *this;
}

Avez-vous besoin de la même chose pour l'opérateur d'affectation des mouvements ? Existe-t-il une situation dans laquelle this == &rhs serait vrai ?

? Class::operator=(Class&& rhs) {
    ?
}

12 votes

Sans rapport avec la question posée, et juste pour que les nouveaux utilisateurs qui lisent cette question dans le futur (car je sais que Seth le sait déjà) ne se fassent pas de fausses idées, Copie et échange est la manière correcte d'implémenter l'opérateur d'assignation de copie dans lequel vous n'avez pas besoin de vérifier l'auto-assignation et-all.

0 votes

La signature doit-elle être const Class&& o Class&& ?

0 votes

Dans des circonstances normales, l'affectation de mouvements vers/depuis le même objet ne pourrait pas se produire. La référence rvalue signifie que vous effectuez une affectation à partir d'un objet temporaire et que rien d'autre ne devrait y faire référence. Une assertion serait peut-être plus appropriée.

154voto

Howard Hinnant Points 59526

Wow, il y a tellement de choses à nettoyer ici...

Tout d'abord, le Copie et échange n'est pas toujours la bonne façon d'implémenter le Copy Assignment. Presque certainement dans le cas de dumb_array il s'agit d'une solution sous-optimale.

L'utilisation de Copie et échange est pour dumb_array est un exemple classique de mise en place de l'opération la plus coûteuse avec les fonctionnalités les plus complètes à la couche inférieure. C'est parfait pour les clients qui veulent la fonctionnalité la plus complète et qui sont prêts à payer la pénalité de performance. Ils obtiennent exactement ce qu'ils veulent.

Mais c'est désastreux pour les clients qui n'ont pas besoin des fonctionnalités les plus complètes et qui recherchent plutôt les performances les plus élevées. Pour eux dumb_array est juste un autre morceau de logiciel qu'ils doivent réécrire parce qu'il est trop lent. Avait dumb_array Si le projet avait été conçu différemment, il aurait pu satisfaire les deux clients sans qu'aucun d'eux ne soit compromis.

La clé pour satisfaire les deux clients est d'intégrer les opérations les plus rapides au niveau le plus bas, puis d'ajouter une API par-dessus pour obtenir des fonctionnalités plus complètes à un coût plus élevé. Par exemple, vous avez besoin de la garantie d'exception forte, très bien, vous payez pour cela. Vous n'en avez pas besoin ? Voici une solution plus rapide.

Soyons concrets : voici la garantie d'exception rapide et basique de l'opérateur Copy Assignment pour dumb_array :

dumb_array& operator=(const dumb_array& other)
{
    if (this != &other)
    {
        if (mSize != other.mSize)
        {
            delete [] mArray;
            mArray = nullptr;
            mArray = other.mSize ? new int[other.mSize] : nullptr;
            mSize = other.mSize;
        }
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }
    return *this;
}

Explication :

L'une des choses les plus coûteuses que l'on puisse faire sur du matériel moderne est de faire un tour dans le tas. Tout ce que vous pouvez faire pour éviter un voyage vers le tas est du temps et des efforts bien dépensés. Les clients de dumb_array peut vouloir assigner souvent des tableaux de la même taille. Et lorsque c'est le cas, il suffit de faire un memcpy (caché sous std::copy ). Vous ne voulez pas allouer un nouveau tableau de la même taille et ensuite désallouer l'ancien tableau de la même taille !

Maintenant, pour vos clients qui veulent réellement une sécurité d'exception forte :

template <class C>
C&
strong_assign(C& lhs, C rhs)
{
    swap(lhs, rhs);
    return lhs;
}

Ou peut-être que si vous voulez profiter de l'assignation des mouvements dans C++11, cela devrait être :

template <class C>
C&
strong_assign(C& lhs, C rhs)
{
    lhs = std::move(rhs);
    return lhs;
}

Si dumb_array Si les clients de l'entreprise accordent de l'importance à la rapidité, ils doivent faire appel à l'option operator= . S'ils ont besoin d'une forte protection contre les exceptions, ils peuvent faire appel à des algorithmes génériques qui fonctionneront sur une grande variété d'objets et ne devront être implémentés qu'une seule fois.

Revenons maintenant à la question initiale (qui a un type-o à ce stade) :

Class&
Class::operator=(Class&& rhs)
{
    if (this == &rhs)  // is this check needed?
    {
       // ...
    }
    return *this;
}

Il s'agit en fait d'une question controversée. Certains diront oui, absolument, d'autres diront non.

Mon avis personnel est que non, vous n'avez pas besoin de ce contrôle.

Justification :

Lorsqu'un objet se lie à une référence rvalue, il y a deux possibilités :

  1. Un temporaire.
  2. Un objet que l'appelant veut vous faire croire qu'il est temporaire.

Si vous avez une référence à un objet qui est un temporaire réel, alors par définition, vous avez une référence unique à cet objet. Il ne peut pas être référencé par n'importe qui d'autre dans tout votre programme. C'est-à-dire que this == &temporary n'est pas possible .

Maintenant, si votre client vous a menti et vous a promis que vous recevriez un temporaire alors que ce n'est pas le cas, alors il est de la responsabilité du client de s'assurer que vous n'avez pas à vous en soucier. Si vous voulez être vraiment prudent, je crois que ce serait une meilleure mise en œuvre :

Class&
Class::operator=(Class&& other)
{
    assert(this != &other);
    // ...
    return *this;
}

C'est-à-dire que si vous sont passé une auto-référence, il s'agit d'un bug de la part du client qui devrait être corrigé.

Pour être complet, voici un opérateur d'assignation de déplacement pour dumb_array :

dumb_array& operator=(dumb_array&& other)
{
    assert(this != &other);
    delete [] mArray;
    mSize = other.mSize;
    mArray = other.mArray;
    other.mSize = 0;
    other.mArray = nullptr;
    return *this;
}

Dans le cas typique d'une affectation de déménagement, *this sera un objet déplacé et donc delete [] mArray; ne doit pas être une option. Il est essentiel que les implémentations rendent la suppression d'un nullptr aussi rapide que possible.

Attention :

Certains diront que swap(x, x) est une bonne idée, ou juste un mal nécessaire. Et ceci, si le swap va vers le swap par défaut, peut provoquer un auto-mouvement d'affectation.

Je ne suis pas d'accord avec le fait que swap(x, x) es jamais une bonne idée. Si je le trouve dans mon propre code, je le considérerai comme un bug de performance et je le corrigerai. Mais si vous voulez l'autoriser, sachez que swap(x, x) ne fait que de l'auto-mouvement-assignation sur une valeur déplacée. Et dans notre dumb_array Par exemple, cela sera parfaitement inoffensif si nous omettons simplement l'assertion, ou si nous la limitons au cas de déplacement de l'objet :

dumb_array& operator=(dumb_array&& other)
{
    assert(this != &other || mSize == 0);
    delete [] mArray;
    mSize = other.mSize;
    mArray = other.mArray;
    other.mSize = 0;
    other.mArray = nullptr;
    return *this;
}

Si vous attribuez vous-même deux déplacés de (vide) dumb_array vous ne faites rien d'incorrect, à part insérer des instructions inutiles dans votre programme. Cette même observation peut être faite pour la grande majorité des objets.

< Mise à jour >

J'ai réfléchi à cette question et j'ai quelque peu modifié ma position. Je pense maintenant que l'assignation doit être tolérée pour l'auto-assignation, mais que les conditions d'affectation sur l'assignation de copie et l'assignation de déplacement sont différentes :

Pour l'attribution des copies :

x = y;

on devrait avoir une post-condition que la valeur de y ne doivent pas être modifiées. Lorsque &x == &y alors cette postcondition se traduit par : l'affectation de la copie de soi ne doit avoir aucun impact sur la valeur de x .

Pour l'affectation des mouvements :

x = std::move(y);

on devrait avoir une post-condition qui y a un état valide mais non spécifié. Lorsque &x == &y alors cette postcondition se traduit par : x a un état valide mais non spécifié. C'est-à-dire que l'assignation du mouvement personnel ne doit pas nécessairement être un no-op. Mais il ne doit pas se planter. Cette post-condition est cohérente avec l'autorisation de swap(x, x) pour travailler :

template <class T>
void
swap(T& x, T& y)
{
    // assume &x == &y
    T tmp(std::move(x));
    // x and y now have a valid but unspecified state
    x = std::move(y);
    // x and y still have a valid but unspecified state
    y = std::move(tmp);
    // x and y have the value of tmp, which is the value they had on entry
}

Ce qui précède fonctionne, à condition que x = std::move(x) ne s'écrase pas. Il peut laisser x dans n'importe quel état valide mais non spécifié.

Je vois trois façons de programmer l'opérateur d'affectation de déplacement pour dumb_array pour y parvenir :

dumb_array& operator=(dumb_array&& other)
{
    delete [] mArray;
    // set *this to a valid state before continuing
    mSize = 0;
    mArray = nullptr;
    // *this is now in a valid state, continue with move assignment
    mSize = other.mSize;
    mArray = other.mArray;
    other.mSize = 0;
    other.mArray = nullptr;
    return *this;
}

L'implémentation ci-dessus tolère l'auto-assignation, mais *this y other finissent par être un tableau de taille nulle après l'affectation d'auto-déplacement, quelle que soit la valeur initiale de *this est. C'est bien.

dumb_array& operator=(dumb_array&& other)
{
    if (this != &other)
    {
        delete [] mArray;
        mSize = other.mSize;
        mArray = other.mArray;
        other.mSize = 0;
        other.mArray = nullptr;
    }
    return *this;
}

L'implémentation ci-dessus tolère l'auto-assignation de la même manière que l'opérateur d'affectation par copie, en en faisant un no-op. C'est également très bien.

dumb_array& operator=(dumb_array&& other)
{
    swap(other);
    return *this;
}

Ce qui précède n'est acceptable que si dumb_array ne contient pas de ressources qui doivent être détruites "immédiatement". Par exemple, si la seule ressource est la mémoire, ce qui précède est parfait. Si dumb_array pourrait éventuellement contenir des verrous mutex ou l'état d'ouverture de fichiers, le client pourrait raisonnablement s'attendre à ce que ces ressources sur le côté gauche de l'affectation de déplacement soient immédiatement libérées et donc cette mise en œuvre pourrait être problématique.

Le coût du premier est de deux magasins supplémentaires. Le coût du second est un test-and-branch. Les deux fonctionnent. Les deux répondent à toutes les exigences du tableau 22 MoveAssignable de la norme C++11. La troisième fonctionne également modulo la préoccupation de la non-ressource mémoire.

Ces trois implémentations peuvent avoir des coûts différents en fonction du matériel : quel est le coût d'un branchement ? Y a-t-il beaucoup de registres ou très peu ?

Ce qu'il faut retenir, c'est que l'affectation par déplacement automatique, contrairement à l'affectation par copie automatique, ne doit pas nécessairement préserver la valeur actuelle.

< /Mise à jour >

Une dernière modification (je l'espère) inspirée par le commentaire de Luc Danton :

Si vous écrivez une classe de haut niveau qui ne gère pas directement la mémoire (mais qui peut avoir des bases ou des membres qui le font), alors la meilleure implémentation de l'affectation des déplacements est souvent :

Class& operator=(Class&&) = default;

Ce mouvement affectera chaque base et chaque membre à tour de rôle, et ne comprendra pas de this != &other vérifier. Vous obtiendrez ainsi les meilleures performances et une sécurité de base contre les exceptions, en supposant qu'aucune invariante ne doive être maintenue parmi vos bases et vos membres. Pour vos clients exigeant une forte protection contre les exceptions, dirigez-les vers strong_assign .

6 votes

Je ne sais pas quoi penser de cette réponse. Elle donne l'impression que l'implémentation de telles classes (qui gèrent leur mémoire de manière très explicite) est une chose courante à faire. Il est vrai que lorsque vous faire Pour écrire une telle classe, il faut faire très attention aux garanties de sécurité des exceptions et trouver le point idéal pour que l'interface soit concise mais pratique, mais la question semble demander des conseils généraux.

0 votes

Oui, je n'utilise jamais le copy-and-swap parce que c'est une perte de temps pour les cours qui gèrent des ressources et autres (pourquoi faire une autre copie complète de toutes vos données ?). Et merci, cela répond à ma question.

5 votes

Rejeté pour avoir suggéré que l'assignation de soi devait être modifiée. jamais assert-échouer ou produire un résultat "non spécifié". L'assignation de soi est littéralement le cas le plus facile pour bien faire. Si votre classe se plante sur std::swap(x,x) alors pourquoi devrais-je lui faire confiance pour gérer correctement des opérations plus complexes ?

12voto

CTMacUser Points 772

D'abord, vous vous êtes trompé dans la signature de l'opérateur d'affectation des mouvements. Puisque les déplacements volent des ressources à l'objet source, la source doit être un objet non-commercial. const r-value de référence.

Class &Class::operator=( Class &&rhs ) {
    //...
    return *this;
}

Notez que vous retournez toujours via un (non- const ) l -référence à la valeur.

Pour l'un ou l'autre type d'affectation directe, la norme n'est pas de vérifier l'auto-affectation, mais de s'assurer qu'une auto-affectation n'entraîne pas un accident et un incendie. En général, personne ne fait explicitement x = x o y = std::move(y) mais l'aliasing, notamment par le biais de fonctions multiples, peut entraîner a = b o c = std::move(d) en étant des auto-assignations. Un contrôle explicite de l'auto-attribution, c'est-à-dire this == &rhs qui saute l'essentiel de la fonction lorsqu'elle est vraie est une façon d'assurer la sécurité de l'auto-affectation. Mais c'est l'un des pires moyens, puisqu'il optimise un cas (espérons-le) rare alors qu'il s'agit d'une anti-optimisation pour le cas le plus commun (à cause des branchements et éventuellement des manques dans le cache).

Maintenant, lorsque (au moins) l'un des opérandes est un objet directement temporaire, vous ne pouvez jamais avoir un scénario d'auto-affectation. Certaines personnes préconisent de supposer ce cas et d'optimiser le code pour cela à tel point que le code devient suicidairement stupide quand la supposition est fausse. Je dis que se débarrasser de la vérification du même objet sur les utilisateurs est irresponsable. Nous ne faisons pas valoir cet argument pour l'affectation par copie ; pourquoi inverser la position pour l'affectation par déplacement ?

Prenons un exemple, modifié par un autre répondant :

dumb_array& dumb_array::operator=(const dumb_array& other)
{
    if (mSize != other.mSize)
    {
        delete [] mArray;
        mArray = nullptr;  // clear this...
        mSize = 0u;        // ...and this in case the next line throws
        mArray = other.mSize ? new int[other.mSize] : nullptr;
        mSize = other.mSize;
    }
    std::copy(other.mArray, other.mArray + mSize, mArray);
    return *this;
}

Cette affectation par copie gère l'auto-affectation de manière gracieuse sans vérification explicite. Si les tailles de la source et de la destination diffèrent, alors la désallocation et la réallocation précèdent la copie. Sinon, seule la copie est effectuée. L'auto-affectation n'obtient pas un chemin optimisé, elle se retrouve dans le même chemin que lorsque les tailles source et destination sont égales au départ. La copie n'est techniquement pas nécessaire lorsque les deux objets sont équivalents (y compris lorsqu'il s'agit du même objet), mais c'est le prix à payer lorsqu'on ne fait pas de vérification d'égalité (en termes de valeur ou d'adresse) puisque ladite vérification elle-même serait un gaspillage la plupart du temps. Notez que l'auto-affectation de l'objet ici provoquera une série d'auto-affectations au niveau de l'élément ; le type d'élément doit être sûr pour faire cela.

Comme son exemple source, cette affectation de copie fournit la garantie de sécurité de base des exceptions. Si vous voulez une garantie forte, utilisez l'opérateur d'assignation unifiée de l'exemple original. Copie et échange qui gère à la fois les affectations par copie et par déplacement. Mais le but de cet exemple est de réduire la sécurité d'un rang pour gagner en rapidité. (BTW, nous supposons que les valeurs des éléments individuels sont indépendantes ; qu'il n'y a pas de contrainte invariante limitant certaines valeurs par rapport à d'autres).

Examinons une affectation de déplacement pour ce même type :

class dumb_array
{
    //...
    void swap(dumb_array& other) noexcept
    {
        // Just in case we add UDT members later
        using std::swap;

        // both members are built-in types -> never throw
        swap( this->mArray, other.mArray );
        swap( this->mSize, other.mSize );
    }

    dumb_array& operator=(dumb_array&& other) noexcept
    {
        this->swap( other );
        return *this;
    }
    //...
};

void  swap( dumb_array &l, dumb_array &r ) noexcept  { l.swap( r ); }

Un type interchangeable qui nécessite une personnalisation doit avoir une fonction libre à deux arguments appelée swap dans le même espace de noms que le type. (La restriction de l'espace de noms permet aux appels non qualifiés à l'échange de fonctionner.) Un type de conteneur doit également ajouter une balise publique swap pour correspondre aux conteneurs standard. Si un membre swap n'est pas fournie, alors la fonction libre swap doit probablement être marqué comme un ami du type interchangeable. Si vous personnalisez les mouvements pour utiliser swap Le code standard appelle le code de déplacement du type, ce qui entraînerait une récursion mutuelle infinie pour les types personnalisés par déplacement.

Comme les destructeurs, les fonctions d'échange et les opérations de déplacement ne devraient jamais être lancées si possible, et probablement marquées comme telles (en C++11). Les types et routines de la bibliothèque standard ont des optimisations pour les types de déplacement non-throwable.

Cette première version de l'assignation des mouvements remplit le contrat de base. Les marqueurs de ressources de la source sont transférés à l'objet de destination. Les anciennes ressources ne seront pas perdues puisque l'objet source les gère maintenant. Et l'objet source est laissé dans un état utilisable où d'autres opérations, y compris l'affectation et la destruction, peuvent lui être appliquées.

Notez que ce déplacement est automatiquement sûr pour l'auto-affectation, puisque l'élément swap est. Il est aussi fortement protégé contre les exceptions. Le problème est la rétention inutile de ressources. Les anciennes ressources pour la destination ne sont conceptuellement plus nécessaires, mais ici elles sont encore là uniquement pour que l'objet source puisse rester valide. Si la destruction programmée de l'objet source est très éloignée, nous gaspillons de l'espace ressource, ou pire si l'espace ressource total est limité et que d'autres demandes de ressources se produiront avant que le (nouvel) objet source ne meure officiellement.

C'est cette question qui est à l'origine des conseils controversés des gourous actuels concernant l'autociblage pendant le déplacement. La façon d'écrire l'assignation de mouvement sans ressources persistantes est quelque chose comme :

class dumb_array
{
    //...
    dumb_array& operator=(dumb_array&& other) noexcept
    {
        delete [] this->mArray;  // kill old resources
        this->mArray = other.mArray;
        this->mSize = other.mSize;
        other.mArray = nullptr;  // reset source
        other.mSize = 0u;
        return *this;
    }
    //...
};

La source est réinitialisée aux conditions par défaut, tandis que les anciennes ressources de destination sont détruites. Dans le cas de l'auto-affectation, l'objet actuel finit par se suicider. Le principal moyen de contourner ce problème est d'entourer le code de l'action d'une balise if(this != &other) ou de laisser les clients manger un bloc assert(this != &other) ligne initiale (si vous vous sentez bien).

Une alternative est d'étudier comment rendre l'affectation par copie fortement sécurisée contre les exceptions, sans affectation unifiée, et de l'appliquer à l'affectation par déplacement :

class dumb_array
{
    //...
    dumb_array& operator=(dumb_array&& other) noexcept
    {
        dumb_array  temp{ std::move(other) };

        this->swap( temp );
        return *this;
    }
    //...
};

Lorsque other y this sont distincts, other est vidé par le déménagement vers temp et reste ainsi. Puis this perd ses anciennes ressources au profit de temp tout en obtenant les ressources initialement détenues par other . Alors les vieilles ressources de this sont tués quand temp fait.

Quand l'auto-affectation se produit, le vidage des other a temp vides this également. Ensuite, l'objet cible récupère ses ressources lorsque temp y this swap. La mort de temp réclame un objet vide, ce qui devrait être pratiquement un no-op. Le site this / other conserve ses ressources.

L'assignation des mouvements ne devrait jamais être rejetée tant que la construction des mouvements et l'échange le sont aussi. Le coût de la sécurité pendant l'auto-affectation est de quelques instructions supplémentaires sur les types de bas niveau, qui devraient être noyées par l'appel de deallocation.

0 votes

Avez-vous besoin de vérifier si de la mémoire a été allouée avant d'appeler delete dans votre deuxième bloc de code ?

3 votes

Votre deuxième exemple de code, l'opérateur d'affectation par copie sans contrôle d'auto-affectation, est erroné. std::copy provoque un comportement non défini si les plages de source et de destination se chevauchent (y compris le cas où elles coïncident). Voir C++14 [alg.copy]/3.

6voto

Luc Danton Points 21421

Je suis dans le camp de ceux qui veulent des opérateurs sûrs pour l'auto-assignation, mais qui ne veulent pas écrire des contrôles d'auto-assignation dans les implémentations de operator= . Et en fait, je ne veux même pas mettre en œuvre operator= Je veux que le comportement par défaut fonctionne dès le départ. Les meilleurs membres spéciaux sont ceux qui sont gratuits.

Ceci étant dit, les exigences MoveAssignable présentes dans la norme sont décrites comme suit (de 17.6.3.1 Template argument requirements [utility.arg.requirements], n3290) :

Expression  Return type Return value    Post-condition
t = rv      T&          t               t is equivalent to the value of rv before the assignment

où les espaces réservés sont décrits comme suit : " t [est une] valeur lval modifiable de type T ;" et " rv est une rvalue de type T ;". Notez que ce sont des exigences imposées aux types utilisés comme arguments aux modèles de la bibliothèque standard, mais en regardant ailleurs dans la norme, je remarque que toutes les exigences sur l'affectation des mouvements sont similaires à celle-ci.

Cela signifie que a = std::move(a) doit être "sûr". Si ce dont vous avez besoin est un test d'identité (par ex. this != &other ), alors foncez, sinon vous ne pourrez même pas mettre vos objets en std::vector ! (A moins que vous n'utilisiez pas les membres/opérations qui requièrent MoveAssignable ; mais peu importe). Remarquez qu'avec l'exemple précédent a = std::move(a) alors this == &other tiendra en effet.

0 votes

Pouvez-vous expliquer comment a = std::move(a) ne fonctionnant pas, une classe ne fonctionnerait pas avec std::vector ? Exemple ?

0 votes

@PaulJ.Lucas Appelant std::vector<T>::erase n'est pas autorisé, sauf si T est MoveAssignable. (Pour l'anecdote, IIRC certaines exigences de MoveAssignable ont été assouplies en MoveInsertable à la place dans C++14).

0 votes

OK, donc T doit être MoveAssignable, mais pourquoi erase() jamais dépendre du déplacement d'un élément vers lui-même ?

2voto

Jason Points 20479

Comme votre operator= est écrite, puisque vous avez fait l'argument de la référence de la valeur r const il n'y a aucun moyen de "voler" les pointeurs et de modifier les valeurs de la référence rvalue entrante... vous ne pouvez tout simplement pas la modifier, vous ne pouvez que la lire. Je ne verrais un problème que si vous deviez commencer à appeler delete sur les pointeurs, etc. dans votre this comme vous le feriez dans une lvaue-reference normale. operator= mais cela va à l'encontre de l'intérêt de la version rvalue... c'est-à-dire qu'il semblerait redondant d'utiliser la version rvalue pour effectuer les mêmes opérations que celles normalement laissées à la méthode const -lvalue operator= méthode.

Maintenant, si vous avez défini votre operator= pour prendre un non const rvalue-reference, alors la seule façon dont je pouvais voir qu'une vérification était nécessaire était si vous passiez la méthode this à une fonction qui a intentionnellement retourné une référence rvalue plutôt qu'un temporaire.

Par exemple, supposons que quelqu'un essaie d'écrire un operator+ et utiliser un mélange de références rvalue et de références lvalue afin d'"empêcher" la création de temporaires supplémentaires lors d'une opération d'addition empilée sur le type d'objet :

struct A; //defines operator=(A&& rhs) where it will "steal" the pointers
          //of rhs and set the original pointers of rhs to NULL

A&& operator+(A& rhs, A&& lhs)
{
    //...code

    return std::move(rhs);
}

A&& operator+(A&& rhs, A&&lhs)
{
    //...code

    return std::move(rhs);
}

int main()
{
    A a;

    a = (a + A()) + A(); //calls operator=(A&&) with reference bound to a

    //...rest of code
}

Maintenant, d'après ce que je comprends des références rvalue, faire ce qui précède est déconseillé (c'est-à-dire que vous devriez juste retourner une référence temporaire, non rvalue), mais, si quelqu'un devait encore le faire, alors vous voudriez vérifier que la référence rvalue entrante ne fait pas référence au même objet que la référence rvalue. this pointeur.

0 votes

Notez que "a=std::move(a)" est une façon triviale d'avoir cette situation. Votre réponse est cependant valable.

1 votes

Je suis tout à fait d'accord pour dire que c'est la manière la plus simple, même si je pense que la plupart des gens ne le feront pas intentionnellement :-) ... Gardez cependant à l'esprit que si la référence rvalue est const alors vous ne pouvez que lire à partir de celui-ci, donc le seul besoin de faire une vérification serait si vous décidez dans votre operator=(const T&&) pour effectuer la même ré-initialisation de this que vous feriez dans une operator=(const T&) plutôt qu'une opération de type swapping (c'est-à-dire voler des pointeurs, etc. plutôt que de faire des copies profondes).

2voto

Ma réponse est toujours que l'assignation de mouvement n'a pas à être sauvegardée contre l'assignation de soi, mais elle a une explication différente. Considérez std::unique_ptr. Si je devais en implémenter un, je ferais quelque chose comme ceci :

unique_ptr& operator=(unique_ptr&& x) {
  delete ptr_;
  ptr_ = x.ptr_;
  x.ptr_ = nullptr;
  return *this;
}

Si vous regardez Scott Meyers expliquant cela il fait quelque chose de similaire. (Si vous vous demandez pourquoi ne pas faire le swap - il y a une écriture supplémentaire). Et ce n'est pas sûr pour l'auto-assignation.

C'est parfois regrettable. Envisagez de sortir du vecteur tous les nombres pairs :

src.erase(
  std::partition_copy(src.begin(), src.end(),
                      src.begin(),
                      std::back_inserter(even),
                      [](int num) { return num % 2; }
                      ).first,
  src.end());

C'est correct pour les entiers, mais je ne pense pas que l'on puisse faire fonctionner quelque chose comme ça avec la sémantique des mouvements.

En conclusion, l'affectation des déplacements à l'objet lui-même n'est pas acceptable et il faut y faire attention.

Petite mise à jour.

  1. Je ne suis pas d'accord avec Howard, qui est une mauvaise idée, mais quand même - je pense que le déplacement automatique des objets "déplacés" devrait fonctionner, parce que swap(x, x) devrait fonctionner. Les algorithmes adorent ces choses ! C'est toujours agréable quand un cas particulier fonctionne. (Et je n'ai encore jamais vu un cas où ce n'est pas gratuit. Cela ne veut pas dire qu'il n'existe pas).
  2. C'est de cette manière que l'assignation d'unique_ptrs est implémentée dans libc++ : unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...} C'est sans danger pour les missions de déménagement.
  3. Directives de base Je pense qu'il devrait être possible d'assigner soi-même les tâches.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X