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

126

Dans l'opérateur d'affectation d'une classe, vous devez généralement vérifier si l'objet affecté est l'objet appelant pour ne pas gâcher les choses:

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 de déplacement? Y a-t-il jamais eu une situation où ce this == &rhsserait vrai?

? Class::operator=(Class&& rhs) {
    ?
}
Seth Carnegie
la source
12
Peu importe le Q demandé, et juste pour que les nouveaux utilisateurs qui lisent ce Q dans la chronologie (car je sais que Seth le sait déjà) ne se trompent pas, Copy and Swap est la bonne façon d'implémenter l'opérateur d'affectation de copie dans lequel vous pas besoin de vérifier l'auto-affectation et-tout.
Alok Sauvegardez le
5
@VaughnCato: A a; a = std::move(a);.
Xeo
11
@VaughnCato L'utilisation std::moveest normale. Ensuite, tenez compte de l'aliasing, et lorsque vous êtes profondément dans une pile d'appels et que vous avez une référence à T, et une autre référence à T... allez-vous vérifier l'identité ici? Voulez-vous trouver le premier appel (ou les appels) où documenter que vous ne pouvez pas passer le même argument deux fois prouvera statiquement que ces deux références ne seront pas alias? Ou allez-vous faire fonctionner l'auto-affectation?
Luc Danton
2
@LucDanton Je préférerais une assertion dans l'opérateur d'affectation. Si std :: move était utilisé de telle manière qu'il était possible de se retrouver avec une auto-assignation rvalue, je considérerais cela comme un bogue qui devrait être corrigé.
Vaughn Cato
4
@VaughnCato Un endroit où l'auto-échange est normal est à l'intérieur de std::sortou std::shuffle- à chaque fois que vous échangez les ie et je éléments d'un tableau sans vérifier au i != jpréalable. ( std::swapest implémenté en termes d'attribution de déplacement.)
Quuxplusone

Réponses:

143

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

Premièrement, la copie et l'échange ne sont pas toujours la bonne manière d'implémenter l'affectation de copie. Presque certainement dans le cas de dumb_array, c'est une solution sous-optimale.

L'utilisation de Copy and Swap est dumb_arrayun exemple classique de placement de l'opération la plus coûteuse avec les fonctionnalités les plus complètes au niveau de la couche inférieure. Il est parfait pour les clients qui veulent la fonctionnalité la plus complète et 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 de la fonctionnalité la plus complète et recherchent plutôt les performances les plus élevées. Pour eux, il dumb_arrayn'y a qu'un autre logiciel qu'ils doivent réécrire parce qu'il est trop lent. Avait dumb_arrayété conçu différemment, il aurait pu satisfaire les deux clients sans compromis pour l'un ou l'autre client.

La clé pour satisfaire les deux clients est de créer les opérations les plus rapides au niveau le plus bas, puis d'ajouter une API en plus de cela pour des fonctionnalités plus complètes à plus de frais. Ie 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 l'opérateur d'affectation de copie de garantie d'exception rapide et basique 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 vous puissiez faire sur du matériel moderne est de vous déplacer vers le tas. Tout ce que vous pouvez faire pour éviter un voyage dans le tas est du temps et des efforts bien dépensés. Les clients de dumb_arraypeuvent souhaiter attribuer souvent des tableaux de même taille. Et quand ils le font, tout ce que vous avez à faire est un memcpy(caché sous std::copy). Vous ne voulez pas allouer un nouveau tableau de la même taille, puis désallouer l'ancien 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 souhaitez profiter de l'affectation de déplacement en C ++ 11, cela devrait être:

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

Si dumb_arrayles clients apprécient la vitesse, ils doivent appeler le operator=. S'ils ont besoin d'une sécurité d'exception forte, ils peuvent appeler 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 d'origine (qui a un type-o à ce stade):

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

C'est en fait une question controversée. Certains diront oui, absolument, certains diront non.

Mon opinion personnelle est non, vous n'avez pas besoin de cette vérification.

Raisonnement:

Lorsqu'un objet se lie à une référence rvalue, c'est l'une des deux choses suivantes:

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

Si vous avez une référence à un objet qui est un véritable temporaire, alors par définition, vous avez une référence unique à cet objet. Il ne peut être référencé nulle part ailleurs dans l'ensemble de votre programme. Ie this == &temporary n'est pas possible .

Maintenant, si votre client vous a menti et vous a promis que vous obtiendrez un temporaire alors que vous ne l'êtes pas, 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 pense que ce serait une meilleure implémentation:

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

Autrement dit , si vous êtes passé une référence auto, c'est un bug de la part du client qui doit être fixé.

Pour être complet, voici un opérateur d'affectation 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 d'utilisation typique de l'affectation de déplacement, *thissera un objet déplacé et delete [] mArray;devrait donc être interdit. Il est essentiel que les implémentations effectuent la suppression sur un nullptr aussi rapidement que possible.

Caveat:

Certains diront que swap(x, x)c'est une bonne idée, ou juste un mal nécessaire. Et cela, si le swap va au swap par défaut, peut provoquer une affectation de déplacement automatique.

Je suis en désaccord que swap(x, x)est toujours une bonne idée. S'il se trouve dans mon propre code, je le considérerai comme un bogue de performance et le corrigerai. Mais au cas où vous voudriez l'autoriser, swap(x, x)sachez que self-move-assignemnet ne fait que sur une valeur déplacée. Et dans notre dumb_arrayexemple, ce sera parfaitement inoffensif si nous omettons simplement l'assertion, ou la contraignons au cas déplacé:

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 (vides) dumb_array, vous ne faites rien de mal à part insérer des instructions inutiles dans votre programme. Ce même constat peut être fait pour la grande majorité des objets.

<Mettre à jour>

J'ai réfléchi davantage à cette question et j'ai quelque peu changé ma position. Je crois maintenant que l'affectation devrait être tolérante à l'auto-affectation, mais que les conditions de publication sur l'affectation de copie et l'affectation de déménagement sont différentes:

Pour l'affectation de copie:

x = y;

on devrait avoir une post-condition que la valeur de yne devrait pas être modifiée. Quand &x == &yalors cette post-condition se traduit par: l'affectation de copie automatique ne devrait pas avoir d'impact sur la valeur de x.

Pour l'attribution de déménagement:

x = std::move(y);

on devrait avoir une post-condition qui ya un état valide mais non spécifié. Quand &x == &yalors cette post-condition se traduit par: xa un état valide mais non spécifié. C’est-à-dire que l’affectation d’auto-déménagement ne doit pas nécessairement être interdite. Mais il ne devrait pas s'écraser. Cette post-condition est cohérente pour permettre swap(x, x)de simplement 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, tant x = std::move(x)qu'il ne plante pas. Il peut partir xdans 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_arrayy 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;
}

La mise en œuvre ci - dessus l' affectation automatique tolère, mais *thiset otherfinissent par être un tableau de taille zéro après la cession auto-move, quelle que soit la valeur d' origine *thisest. C'est bon.

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-affectation de la même manière que l'opérateur d'affectation de copie, en en faisant un no-op. C'est bien aussi.

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

Ce qui précède n'est correct que s'il dumb_arrayne 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 convient. S'il dumb_arraypouvait contenir des verrous mutex ou l'état ouvert des fichiers, le client pouvait raisonnablement s'attendre à ce que ces ressources sur les lhs de l'affectation de déplacement soient immédiatement libérées et cette implémentation pourrait donc être problématique.

Le coût du premier est de deux magasins supplémentaires. Le coût du second est un test et une branche. Les deux fonctionnent. Les deux répondent à toutes les exigences du tableau 22 des exigences MoveAssignable dans la norme C ++ 11. Le troisième fonctionne également modulo le problème des ressources sans mémoire.

Les trois implémentations peuvent avoir des coûts différents selon le matériel: quel est le coût d'une succursale? Y a-t-il beaucoup de registres ou très peu?

Ce qu'il faut retenir, c'est que l'affectation de déplacement automatique, contrairement à l'affectation de copie automatique, n'a pas à conserver la valeur actuelle.

</Mettre à jour>

Un dernier montage (espérons-le) inspiré du commentaire de Luc Danton:

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

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

Cela déplacera chaque base et chaque membre à tour de rôle, et n'inclura pas de this != &otherchèque. Cela vous donnera les performances les plus élevées et la sécurité des exceptions de base en supposant qu'aucun invariant ne doit être maintenu parmi vos bases et vos membres. Pour vos clients exigeant une sécurité d'exception forte, dirigez-les vers strong_assign.

Howard Hinnant
la source
6
Je ne sais pas comment ressentir cette réponse. Cela 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. Il est vrai que lorsque vous faites écrire un d' une classe doit être très très prudent sur les garanties de sécurité d'exception et de trouver le sweet spot pour l'interface d'être concis mais pratique, mais la question semble demander des conseils généraux.
Luc Danton
Ouais, je n'utilise certainement jamais la copie et l'échange parce que c'est une perte de temps pour les classes qui gèrent les ressources et les choses (pourquoi faire une autre copie entière de toutes vos données?). Et merci, cela répond à ma question.
Seth Carnegie
5
Voté contre la suggestion selon laquelle le déplacement-assignation-de-soi devrait jamais affirmer-échouer ou produire un résultat "non spécifié". L'affectation de soi est littéralement le cas le plus facile à faire. Si votre classe plante std::swap(x,x), alors pourquoi devrais-je lui faire confiance pour gérer correctement les opérations plus compliquées?
Quuxplusone
1
@Quuxplusone: Je suis arrivé à être d'accord avec vous sur l'assert-fail, comme indiqué dans la mise à jour de ma réponse. Dans la mesure où cela std::swap(x,x)va, cela fonctionne juste même lorsque x = std::move(x)produit un résultat non spécifié. Essayez-le! Tu n'as pas besoin de me croire.
Howard Hinnant
@HowardHinnant bon point, swapfonctionne aussi longtemps que les x = move(x)feuilles xdans n'importe quel état de passage. Et les algorithmes std::copy/ std::movesont définis de manière à produire déjà un comportement indéfini sur les copies no-op (aïe; le jeune de 20 ans memmoveobtient le bon cas mais std::movene le fait pas!). Donc je suppose que je n'ai pas encore pensé à un "slam dunk" pour l'auto-affectation. Mais de toute évidence, l'auto-affectation est quelque chose qui se produit souvent dans le vrai code, que le Standard l'ait béni ou non.
Quuxplusone
11

Tout d'abord, la signature de l'opérateur d'assignation de déplacement est erronée. Puisque les déplacements volent les ressources de l'objet source, la source doit être une constréférence sans valeur r.

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

Notez que vous retournez toujours via une référence (non const) l -value.

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 ne provoque pas de crash-and-burn. Généralement, personne ne fait x = xou y = std::move(y)n'appelle explicitement , mais l'aliasing, en particulier via plusieurs fonctions, peut conduire a = bou c = std::move(d)devenir des auto-assignations. Une vérification explicite de l'auto-affectation, c'est-à-dire this == &rhsqui saute la viande de la fonction lorsqu'elle est vraie, est un moyen d'assurer la sécurité de l'auto-assignation. Mais c'est l'un des pires moyens, car il optimise un cas rare (espérons-le), alors qu'il s'agit d'une anti-optimisation pour le cas le plus courant (en raison de branchements et éventuellement d'erreurs de cache).

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

Faisons 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 de copie gère l'auto-affectation de manière gracieuse sans vérification explicite. Si les tailles source et destination diffèrent, 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 est sauvegardée dans le même chemin que lorsque les tailles source et destination commencent égales. La copie est techniquement inutile lorsque les deux objets sont équivalents (y compris lorsqu'ils sont le même objet), mais c'est le prix à payer si vous ne faites pas de contrôle d'égalité (en valeur ou en adresse) car ledit contrôle lui-même serait un gaspillage le plus du temps. Notez que l'auto-affectation d'objet provoquera ici une série d'auto-affectations au niveau de l'élément; le type d'élément doit être sûr pour ce faire.

Comme son exemple source, cette affectation de copie fournit la garantie de sécurité d'exception de base. Si vous souhaitez bénéficier d'une garantie renforcée, utilisez l'opérateur d'affectation unifiée de la requête Copie et échange d'origine , qui gère à la fois l'affectation de copie et de déplacement. Mais le but de cet exemple est de réduire la sécurité d'un rang pour gagner en vitesse. (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.)

Regardons 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 échangeable qui nécessite une personnalisation doit avoir une fonction sans deux arguments appelée swapdans le même espace de noms que le type. (La restriction d'espace de noms permet aux appels non qualifiés de permuter de fonctionner.) Un type de conteneur doit également ajouter une fonction swapmembre public pour correspondre aux conteneurs standard. Si un membre swapn'est pas fourni, alors la fonction libre swapdoit probablement être marquée comme un ami du type échangeable. Si vous personnalisez les mouvements à utiliser swap, vous devez fournir votre propre code d'échange; le code standard appelle le code de déplacement du type, ce qui entraînerait une récurrence 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 doivent jamais être lancées si possible, et probablement marquées comme telles (en C ++ 11). Les types de bibliothèques et les routines standard ont des optimisations pour les types de déplacement non jetables.

Cette première version de l'attribution de déménagement remplit le contrat de base. Les marqueurs de ressources de la source sont transférés vers l'objet de destination. Les anciennes ressources ne seront pas divulguées puisque l'objet source les gère désormais. 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 cette affectation de déplacement est automatiquement sûre pour l'auto-affectation, puisque l' swapappel l'est. C'est aussi fortement exceptionnellement sûr. Le problème est la rétention inutile des ressources. Les anciennes ressources pour la destination ne sont plus nécessaires du point de vue conceptuel, mais ici, elles ne sont toujours disponibles que pour que l'objet source puisse rester valide. Si la destruction programmée de l'objet source est loin, nous gaspillons de l'espace de ressources, ou pire si l'espace de ressources total est limité et que d'autres requêtes de ressources se produiront avant que le (nouvel) objet source ne meure officiellement.

Ce problème est à l'origine des conseils controversés des gourous actuels concernant l'auto-ciblage lors de l'attribution des déplacements. La façon d'écrire une affectation de déplacement 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, votre objet actuel finit par se suicider. Le principal moyen de contourner ce problème est d'entourer le code d'action d'un if(this != &other)bloc, ou de le visser et de laisser les clients manger une assert(this != &other)première ligne (si vous vous sentez bien).

Une alternative consiste à étudier comment rendre l'attribution de copie fortement sécurisée pour les exceptions, sans assignation unifiée, et l'appliquer à l'attribution de mouvement:

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

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

Quand otheret thissont distincts, otherest vidé par le passage à tempet le reste. Puis thisperd ses anciennes ressources temptout en récupérant les ressources initialement détenues par other. Ensuite, les anciennes ressources thissont tuées quand temp.

Lorsque l'auto-affectation se produit, le vidage de othervers tempse vide thiségalement. Ensuite, l'objet cible récupère ses ressources quand tempet thiséchange. La mort de tempréclame un objet vide, qui devrait être pratiquement un no-op. L' objet this/ otherconserve ses ressources.

L'assignation de mouvement ne doit jamais être lancée tant que la construction de mouvement et l'échange le sont également. Le coût d'être également en sécurité pendant l'auto-affectation est quelques instructions supplémentaires par rapport aux types de bas niveau, qui devraient être submergées par l'appel de désallocation.

CTMacUser
la source
Avez-vous besoin de vérifier si de la mémoire a été allouée avant d'appeler deletevotre deuxième bloc de code?
user3728501
3
Votre deuxième exemple de code, l'opérateur d'affectation de copie sans contrôle d'auto-affectation, est incorrect. std::copyprovoque un comportement indéfini si les plages source et destination se chevauchent (y compris le cas où elles coïncident). Voir C ++ 14 [alg.copy] / 3.
MM
6

Je suis dans le camp de ceux qui veulent des opérateurs sûrs pour l'auto-affectation, mais ne veulent pas écrire de chèques d'auto-affectation dans les implémentations de operator=. Et en fait, je ne veux même pas du tout mettre operator=en œuvre , je veux que le comportement par défaut fonctionne «dès la sortie de la boîte». Les meilleurs membres spéciaux sont ceux qui viennent gratuitement.

Cela étant dit, les exigences MoveAssignable présentes dans la norme sont décrites comme suit (à partir de 17.6.3.1 Exigences d'argument de modèle [utility.arg.requirements], n3290):

Expression Type de retour Valeur de retour Post-condition
t = rv T & tt équivaut à la valeur de rv avant l'affectation

où les espaces réservés sont décrits comme tsuit : " [est une] valeur l modifiable de type T;" et " rvest une rvaleur de type T;". Notez que ce sont des exigences placées sur les types utilisés comme arguments dans les modèles de la bibliothèque Standard, mais en regardant ailleurs dans la norme, je remarque que chaque exigence relative à l'affectation de déplacement est similaire à celle-ci.

Cela signifie que cela a = std::move(a)doit être «sûr». Si vous avez besoin d'un test d'identité (par exemple this != &other), allez-y, sinon vous ne pourrez même pas y mettre vos objets std::vector! (À moins que vous n'utilisiez les membres / opérations qui nécessitent MoveAssignable; mais ne le pensez pas.) Notez qu'avec l'exemple précédent a = std::move(a), this == &othercela tiendra effectivement.

Luc Danton
la source
Pouvez-vous expliquer comment le fait de a = std::move(a)ne pas travailler empêcherait une classe de travailler avec std::vector? Exemple?
Paul
@ PaulJ.Lucas L'appel std::vector<T>::erasen'est pas autorisé sauf si TMoveAssignable. (En marge de l'IIRC, certaines exigences de MoveAssignable ont été assouplies pour MoveInsertable à la place en C ++ 14.)
Luc Danton
OK, Tdoit donc être MoveAssignable, mais pourquoi erase()dépendrait-il du déplacement d'un élément vers lui - même ?
Paul
@ PaulJ.Lucas Il n'y a pas de réponse satisfaisante à cette question. Tout se résume à «ne pas rompre les contrats».
Luc Danton
2

Pendant que votre operator=fonction actuelle est écrite, puisque vous avez fait l'argument rvalue-reference const, il n'y a aucun moyen que vous puissiez "voler" les pointeurs et changer les valeurs de la référence rvalue entrante ... vous ne pouvez tout simplement pas le changer, vous ne pouvait que lire. Je ne verrais un problème que si vous deviez commencer à appeler des deletepointeurs, etc. dans votre thisobjet comme vous le feriez dans une operator=méthode de référence lvaue normale , mais cela va à l'encontre du point de la rvalue-version ... c'est-à-dire que ce serait semble redondant d'utiliser la version rvalue pour effectuer fondamentalement les mêmes opérations normalement laissées à une méthode const-lvalue operator=.

Maintenant, si vous avez défini votre operator=pour prendre une constréférence non -rvalue, alors la seule façon pour moi de voir une vérification requise était de passer l' thisobjet à une fonction qui renvoyait intentionnellement une référence rvalue plutôt qu'une référence temporaire.

Par exemple, supposons que quelqu'un ait essayé d'écrire une operator+fonction et d'utiliser un mélange de références rvalue et de références lvalue afin d '"empêcher" la création de temporalités 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, il est déconseillé de faire ce qui précède (c'est-à-dire, vous devriez simplement renvoyer une référence temporaire, pas rvalue), mais, si quelqu'un devait encore faire cela, alors vous voudriez vérifier pour faire sûr que la rvalue-reference entrante ne faisait pas référence au même objet que le thispointeur.

Jason
la source
Notez que "a = std :: move (a)" est une manière triviale d'avoir cette situation. Votre réponse est cependant valable.
Vaughn Cato
1
Tout à fait d'accord que c'est le moyen le plus simple, même si je pense que la plupart des gens ne le feront pas intentionnellement :-) ... Gardez à l'esprit que si la rvalue-reference est const, alors vous ne pouvez lire qu'à partir de celle-ci, donc le seul besoin de faire une vérification serait si vous décidiez dans votre operator=(const T&&)pour effectuer la même réinitialisation thisque vous feriez dans une operator=(const T&)méthode typique plutôt qu'une opération de style swapping (c'est-à-dire, voler des pointeurs, etc. plutôt que de faire des copies complètes).
Jason
1

Ma réponse est toujours que l'attribution de déménagement ne doit pas être sauvegardée contre l'auto-assignation, 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 expliquer cela, il fait quelque chose de similaire. (Si vous vous promenez pourquoi ne pas échanger - il a une écriture supplémentaire). Et ce n'est pas sûr pour l'auto-affectation.

Parfois c'est malheureux. 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 vous puissiez faire quelque chose comme ça avec la sémantique de déplacement.

Pour conclure: déplacer l'affectation vers l'objet lui-même n'est pas correct et il faut y faire attention.

Petite mise à jour.

  1. Je ne suis pas d'accord avec Howard, ce qui est une mauvaise idée, mais quand même - je pense que l'attribution de mouvements autonomes d'objets "déplacés" devrait fonctionner, parce qu'elle swap(x, x)devrait fonctionner. Les algorithmes adorent ces choses! C'est toujours agréable quand une valise d'angle fonctionne. (Et je n'ai pas encore vu de cas où ce n'est pas gratuit. Cela ne veut pas dire que ça n'existe pas).
  2. C'est ainsi que l'assignation d'unique_ptrs est implémentée dans libc ++: unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...} c'est sûr pour l'attribution de déplacement automatique.
  3. Les directives de base pensent qu'il devrait être acceptable de se déplacer soi-même.
Denis Yaroshevskiy
la source
0

Il y a une situation à laquelle (ce == rhs) je peux penser. Pour cette déclaration: Myclass obj; std :: move (obj) = std :: move (obj)

petit monstre
la source
Myclass obj; std :: move (obj) = std :: move (obj);
little_monster