web-dev-qa-db-fra.com

Quel est l'idiome copie-et-swap?

Quel est cet idiome et quand devrait-il être utilisé? Quels problèmes cela résout-il? L'idiome change-t-il lorsque C++ 11 est utilisé?

Bien que cela ait été mentionné dans de nombreux endroits, nous n'avions pas de question et de réponse "Qu'est-ce que c'est", alors la voici. Voici une liste partielle des endroits où il a été mentionné précédemment:

1873
GManNickG

L’affectation, en son cœur, est composée de deux étapes: détruit l’ancien état de l’objet et construisant son nouvel état en tant que copie de l'état d'un autre objet.

En gros, c’est ce que le constructeur ) et le constructeur ) , la première idée serait donc de leur déléguer le travail. Cependant, étant donné que la destruction ne doit pas échouer, alors que la construction le permet, , nous souhaitons le faire dans l’inverse : exécute d'abord la partie constructive et si cela réussit, fait alors la partie destructive . L'idiome copy-and-swap est un moyen de le faire: il appelle d'abord le constructeur de copie d'une classe pour créer un temporaire, puis échange ses données avec celles du temporaire, puis laisse le destructeur de ce dernier détruire l'ancien état.
Puisque swap() est censé ne jamais échouer, la seule partie qui pourrait échouer est la construction de copie. Cela est effectué en premier et si cela échoue, rien ne sera modifié dans l'objet ciblé.

Dans sa forme affinée, copier et permuter est implémenté en effectuant la copie en initialisant le paramètre (non référencé) de l'opérateur d'affectation:

T& operator=(T tmp)
{
    this->swap(tmp);
    return *this;
}
257
sbi

Il y a déjà de bonnes réponses. Je vais me concentrer principalement sur ce qui me semble leur faire défaut - une explication des "inconvénients" avec l'idiome copie-échange ...

Quel est l'idiome copie-et-swap?

Une façon d'implémenter l'opérateur d'affectation sous la forme d'une fonction swap:

X& operator=(X rhs)
{
    swap(rhs);
    return *this;
}

L'idée fondamentale est que:

  • la partie la plus sujette aux erreurs de l'affectation à un objet est de s'assurer que toutes les ressources dont le nouvel état a besoin sont acquises (par exemple, mémoire, descripteurs)

  • cette acquisition peut être tentée avant de modifier l'état actuel de l'objet (c'est-à-dire *this) si une copie de la nouvelle valeur est faite, laquelle C’est pourquoi rhs est accepté par valeur (c'est-à-dire copié) plutôt que par par référence

  • permuter l’état de la copie locale rhs et *this est généralement relativement facile à faire sans risque d’échec/exception, étant donné le la copie locale n'a besoin d'aucun état particulier par la suite (il suffit simplement d'un état ajustable pour que le destructeur s'exécute, comme pour un objet en cours déplacé depuis>> C++ 11)

Quand doit-il être utilisé? (Quels problèmes cela résout-il [/ create]?)

  • Lorsque vous souhaitez que l'objet assigné à ne soit pas affecté par une affectation qui lève une exception, supposons que vous ayez ou pouvez écrire un swap avec une garantie forte d'exception et, idéalement, un échec/throw .. †

  • Lorsque vous voulez un moyen simple, facile à comprendre et robuste de définir l’opérateur d’affectation en termes de (plus simple) constructeur de copie, de fonctions swap et de destructeur.

    • L'auto-affectation effectuée sous forme de copie-échange évite les cas Edge souvent négligés. ‡
  • Lorsqu'une dégradation des performances ou une utilisation momentanément supérieure des ressources créée par la présence d'un objet temporaire supplémentaire au cours de l'affectation n'est pas importante pour votre application. ⁂

swap projection: il est généralement possible d'échanger de manière fiable des membres de données que les objets suivent par pointeur, mais des membres de données autres que des pointeurs qui ne disposent pas d'un échange immédiat, ou pour lesquels l'échange doit être implémenté sous la forme X tmp = lhs; lhs = rhs; rhs = tmp; et la construction ou l'attribution de copie peuvent avoir lieu, tout en ayant le potentiel d'échouer, laissant certains membres de données échangés et d'autres non. Ce potentiel s'applique même aux C++ 03 std::string comme James commente une autre réponse:

@wilhelmtell: En C++ 03, il n'est pas fait mention d'exceptions potentiellement levées par std :: string :: swap (appelé par std :: swap). En C++ 0x, std :: string :: swap est noexcept et ne doit pas renvoyer d'exceptions. - James McNellis 22 déc. 10 à 15:24


‡ Une implémentation d'opérateur d'affectation qui semble saine lors de l'affectation à partir d'un objet distinct peut facilement échouer pour une auto-affectation. Bien qu'il puisse sembler inimaginable que le code client tente même une auto-affectation, cela peut se produire relativement facilement lors d'opérations algo sur des conteneurs, avec x = f(x); code où f est (peut-être uniquement pour certains #ifdef branches) une macro ala #define f(x) x ou une fonction renvoyant une référence à x, ou même un code (probablement inefficace mais concis) comme x = c1 ? x * 2 : c2 ? x / 2 : x;). Par exemple:

struct X
{
    T* p_;
    size_t size_;
    X& operator=(const X& rhs)
    {
        delete[] p_;  // OUCH!
        p_ = new T[size_ = rhs.size_];
        std::copy(p_, rhs.p_, rhs.p_ + rhs.size_);
    }
    ...
};

Lors de l'affectation automatique, le code ci-dessus supprime x.p_;, pointe p_ sur une région de segment nouvellement allouée, puis tente de lire le non initialisé des données qui s'y trouvent (comportement indéfini), si cela ne fait rien de trop bizarre, copy tente une auto-affectation à chaque 'T' juste détruit!


Id L'idiome copier-permuter peut introduire des inefficacités ou des limitations dues à l'utilisation d'un temporaire supplémentaire (lorsque le paramètre de l'opérateur est construit en copie):

struct Client
{
    IP_Address ip_address_;
    int socket_;
    X(const X& rhs)
      : ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_))
    { }
};

Ici, un Client::operator= écrit à la main pourrait vérifier si *this est déjà connecté au même serveur que rhs (en envoyant éventuellement un code de "réinitialisation" si nécessaire), alors que le L’approche d’échange invoquerait le constructeur de copie, qui serait probablement écrit pour ouvrir une connexion de socket distincte, puis fermer celle d’origine. Cela pourrait non seulement signifier une interaction réseau à distance au lieu d'une simple copie de variable en cours de processus, mais aussi enfreindre les limites imposées par le client ou le serveur aux ressources ou connexions de socket. (Bien sûr, cette classe a une interface assez horrible, mais c'est une autre affaire ;-P).

38
Tony Delroy

Cette réponse s'apparente davantage à un ajout et à une légère modification des réponses ci-dessus.

Dans certaines versions de Visual Studio (et éventuellement d’autres compilateurs), il existe un bogue qui est vraiment gênant et qui n’a pas de sens. Donc, si vous déclarez/définissez votre fonction swap comme ceci:

friend void swap(A& first, A& second) {

    std::swap(first.size, second.size);
    std::swap(first.arr, second.arr);

}

... le compilateur vous hurle dessus lorsque vous appelez la fonction swap:

enter image description here

Cela a quelque chose à voir avec une fonction friend appelée et un objet this transmis en tant que paramètre.


Une solution consiste à ne pas utiliser le mot clé friend et à redéfinir la fonction swap:

void swap(A& other) {

    std::swap(size, other.size);
    std::swap(arr, other.arr);

}

Cette fois, vous pouvez simplement appeler swap et passer other, rendant ainsi le compilateur heureux:

enter image description here


Après tout, vous n'avez pas besoin d'utiliser une fonction friend pour échanger 2 objets. Il est tout aussi logique de faire de swap une fonction membre ayant un objet other en tant que paramètre.

Vous avez déjà accès à l'objet this, il est donc techniquement redondant de le saisir.

22
Oleksiy

J'aimerais ajouter un mot d'avertissement lorsque vous utilisez des conteneurs à capacité allocateur de type C++ 11. L'échange et l'assignation ont une sémantique légèrement différente.

Pour être concret, considérons un conteneur std::vector<T, A>, où A est un type d’allocateur avec état, et nous comparerons les fonctions suivantes:

void fs(std::vector<T, A> & a, std::vector<T, A> & b)
{ 
    a.swap(b);
    b.clear(); // not important what you do with b
}

void fm(std::vector<T, A> & a, std::vector<T, A> & b)
{
    a = std::move(b);
}

Le but des deux fonctions fs et fm est de donner a l'état que b avait initialement. Cependant, il y a une question cachée: que se passe-t-il si a.get_allocator() != b.get_allocator()? La réponse est: ça dépend. Écrivons AT = std::allocator_traits<A>.

  • Si AT::propagate_on_container_move_assignment est std::true_type, alors fm réaffecte l'allocateur de a avec la valeur de b.get_allocator(), sinon il ne le fait pas et a continue d'utiliser son allocateur d'origine. Dans ce cas, les éléments de données doivent être échangés individuellement, car le stockage de a et b n'est pas compatible.

  • Si AT::propagate_on_container_swap est std::true_type, alors fs permute les données et les allocateurs de la manière attendue.

  • Si AT::propagate_on_container_swap est std::false_type, nous avons besoin d'une vérification dynamique.

    • Si a.get_allocator() == b.get_allocator(), les deux conteneurs utilisent un stockage compatible et la permutation s'effectue de la manière habituelle.
    • Cependant, si a.get_allocator() != b.get_allocator(), le programme a comportement indéfini (cf. [conteneur.requirements.general/8].

Le résultat est que la permutation est devenue une opération non triviale en C++ 11 dès que votre conteneur prend en charge les allocateurs avec état. C'est un "cas d'utilisation avancé", mais ce n'est pas totalement improbable, car les optimisations de déplacement ne deviennent intéressantes que lorsque votre classe gère une ressource et la mémoire est l'une des ressources les plus populaires.

13
Kerrek SB