Qu'est-ce que l'idiome de copie et d'échange?

2003

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

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

GManNickG
la source
7
gotw.ca/gotw/059.htm de Herb Sutter
DumbCoder
2
Génial, j'ai lié cette question de ma réponse pour déplacer la sémantique .
fredoverflow
4
Bonne idée d'avoir une explication complète de cet idiome, c'est tellement courant que tout le monde devrait le savoir.
Matthieu M.
16
Avertissement: l'idiome de copie / échange est utilisé beaucoup plus fréquemment qu'il n'est utile. Il est souvent préjudiciable aux performances lorsqu'une garantie de sécurité d'exception forte n'est pas nécessaire lors de l'attribution de copie. Et lorsqu'une sécurité d'exception élevée est nécessaire pour l'affectation de copie, elle est facilement fournie par une fonction générique courte, en plus d'un opérateur d'affectation de copie beaucoup plus rapide. Voir slideshare.net/ripplelabs/howard-hinnant-accu2014 diapositives 43 à 53. Résumé: copier / échanger est un outil utile dans la boîte à outils. Mais il a été sur-commercialisé et par la suite a souvent été abusé.
Howard Hinnant
2
@HowardHinnant: Oui, +1 à cela. J'ai écrit cela à un moment où presque toutes les questions C ++ étaient "aider ma classe à se bloquer quand une copie" et c'était ma réponse. C'est approprié lorsque vous voulez juste travailler sur la sémantique de copie / déplacement ou autre pour que vous puissiez passer à autre chose, mais ce n'est pas vraiment optimal. N'hésitez pas à mettre un avertissement en haut de ma réponse si vous pensez que cela vous aidera.
GManNickG

Réponses:

2184

Aperçu

Pourquoi avons-nous besoin de l'idiome de copie et d'échange?

Toute classe qui gère une ressource (un wrapper , comme un pointeur intelligent) doit implémenter The Big Three . Alors que les objectifs et la mise en œuvre du constructeur et du destructeur de copie sont simples, l'opérateur d'affectation de copie est sans doute le plus nuancé et le plus difficile. Comment faut-il procéder? Quels pièges faut-il éviter?

le idiome de copie et d'échange est la solution et aide élégamment l'opérateur d'affectation à réaliser deux choses: éviter la duplication de code et fournir une garantie d'exception forte .

Comment ça marche?

Conceptuellement , il fonctionne en utilisant la fonctionnalité du constructeur de copie pour créer une copie locale des données, puis prend les données copiées avec une swapfonction, échangeant les anciennes données avec les nouvelles données. La copie temporaire se détruit ensuite, emportant les anciennes données avec elle. Il nous reste une copie des nouvelles données.

Pour utiliser l'idiome de copie et d'échange, nous avons besoin de trois choses: un constructeur de copie de travail, un destructeur de travail (les deux sont la base de tout wrapper, donc devraient être complets de toute façon) et une swapfonction.

Une fonction d'échange est un non-lancement qui permute deux objets d'une classe, membre pour membre. Nous pourrions être tentés d'utiliser std::swapau lieu de fournir les nôtres, mais cela serait impossible; std::swaputilise le constructeur de copie et l'opérateur d'affectation de copie dans son implémentation, et nous essaierions finalement de définir l'opérateur d'affectation en fonction de lui-même!

(Non seulement cela, mais les appels non qualifiés à swaputiliseront notre opérateur d'échange personnalisé, ignorant la construction et la destruction inutiles de notre classe quistd::swap impliqueraient.)


Une explication approfondie

Le but

Prenons un cas concret. Nous voulons gérer, dans une classe par ailleurs inutile, un tableau dynamique. Nous commençons avec un constructeur, un constructeur de copie et un destructeur qui fonctionnent:

#include <algorithm> // std::copy
#include <cstddef> // std::size_t

class dumb_array
{
public:
    // (default) constructor
    dumb_array(std::size_t size = 0)
        : mSize(size),
          mArray(mSize ? new int[mSize]() : nullptr)
    {
    }

    // copy-constructor
    dumb_array(const dumb_array& other)
        : mSize(other.mSize),
          mArray(mSize ? new int[mSize] : nullptr),
    {
        // note that this is non-throwing, because of the data
        // types being used; more attention to detail with regards
        // to exceptions must be given in a more general case, however
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }

    // destructor
    ~dumb_array()
    {
        delete [] mArray;
    }

private:
    std::size_t mSize;
    int* mArray;
};

Cette classe gère presque le tableau avec succès, mais elle a besoin operator= fonctionner correctement.

Une solution ratée

Voici à quoi pourrait ressembler une implémentation naïve:

// the hard part
dumb_array& operator=(const dumb_array& other)
{
    if (this != &other) // (1)
    {
        // get rid of the old data...
        delete [] mArray; // (2)
        mArray = nullptr; // (2) *(see footnote for rationale)

        // ...and put in the new
        mSize = other.mSize; // (3)
        mArray = mSize ? new int[mSize] : nullptr; // (3)
        std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
    }

    return *this;
}

Et nous disons que nous avons terminé; cela gère désormais un tableau, sans fuites. Cependant, il souffre de trois problèmes, marqués séquentiellement dans le code comme (n).

  1. Le premier est le test d'auto-affectation. Cette vérification sert deux objectifs: c'est un moyen facile de nous empêcher d'exécuter du code inutile lors de l'auto-affectation, et il nous protège des bogues subtils (comme la suppression du tableau uniquement pour essayer de le copier). Mais dans tous les autres cas, cela sert simplement à ralentir le programme et à agir comme du bruit dans le code; l'auto-affectation se produit rarement, donc la plupart du temps ce contrôle est un gaspillage. Il serait préférable que l'opérateur puisse fonctionner correctement sans lui.

  2. Le second est qu'il ne fournit qu'une garantie d'exception de base. En cas d' new int[mSize]échec, *thisaura été modifié. (À savoir, la taille est incorrecte et les données ont disparu!) Pour une garantie d'exception forte, il faudrait que cela ressemble à:

    dumb_array& operator=(const dumb_array& other)
    {
        if (this != &other) // (1)
        {
            // get the new data ready before we replace the old
            std::size_t newSize = other.mSize;
            int* newArray = newSize ? new int[newSize]() : nullptr; // (3)
            std::copy(other.mArray, other.mArray + newSize, newArray); // (3)
    
            // replace the old data (all are non-throwing)
            delete [] mArray;
            mSize = newSize;
            mArray = newArray;
        }
    
        return *this;
    }
  3. Le code s'est étendu! Ce qui nous amène au troisième problème: la duplication de code. Notre opérateur d'affectation duplique efficacement tout le code que nous avons déjà écrit ailleurs, et c'est une chose terrible.

Dans notre cas, le cœur de celui-ci n'est que de deux lignes (l'allocation et la copie), mais avec des ressources plus complexes, ce ballonnement de code peut être assez compliqué. Nous devons nous efforcer de ne jamais nous répéter.

(On pourrait se demander: si autant de code est nécessaire pour gérer correctement une ressource, que se passe-t-il si ma classe en gère plus d'une? Bien que cela puisse sembler être une préoccupation valide, et en effet cela nécessite des clauses try/ non triviales catch, c'est un non C'est parce qu'une classe ne doit gérer qu'une seule ressource !)

Une solution réussie

Comme mentionné, l'idiome de copie et d'échange résoudra tous ces problèmes. Mais en ce moment, nous avons toutes les exigences sauf une: une swapfonction. Bien que la règle des trois implique avec succès l'existence de notre constructeur de copie, opérateur d'affectation et destructeur, elle devrait vraiment s'appeler "Les trois grands et demi": chaque fois que votre classe gère une ressource, il est également logique de fournir une swapfonction .

Nous devons ajouter des fonctionnalités de swap à notre classe, et nous le faisons comme suit †:

class dumb_array
{
public:
    // ...

    friend void swap(dumb_array& first, dumb_array& second) // nothrow
    {
        // enable ADL (not necessary in our case, but good practice)
        using std::swap;

        // by swapping the members of two objects,
        // the two objects are effectively swapped
        swap(first.mSize, second.mSize);
        swap(first.mArray, second.mArray);
    }

    // ...
};

( Voici l'explication pourquoi public friend swap.) Maintenant, non seulement nous pouvons échanger les nôtres dumb_array, mais les échanges en général peuvent être plus efficaces; il échange simplement des pointeurs et des tailles, plutôt que d'allouer et de copier des tableaux entiers. Mis à part ce bonus de fonctionnalité et d'efficacité, nous sommes maintenant prêts à implémenter l'idiome de copie et d'échange.

Sans plus tarder, notre opérateur d'affectation est:

dumb_array& operator=(dumb_array other) // (1)
{
    swap(*this, other); // (2)

    return *this;
}

Et c'est tout! D'un seul coup, les trois problèmes sont résolus avec élégance à la fois.

Pourquoi ça marche?

On remarque d'abord un choix important: l'argument paramètre est pris par valeur . Alors que l'on pourrait tout aussi facilement faire ce qui suit (et en effet, de nombreuses implémentations naïves de l'idiome le font):

dumb_array& operator=(const dumb_array& other)
{
    dumb_array temp(other);
    swap(*this, temp);

    return *this;
}

Nous perdons une importante opportunité d'optimisation . Non seulement cela, mais ce choix est critique en C ++ 11, qui est discuté plus tard. (D'une manière générale, une directive remarquablement utile est la suivante: si vous allez faire une copie de quelque chose dans une fonction, laissez le compilateur le faire dans la liste des paramètres. ‡)

Quoi qu'il en soit, cette méthode d'obtention de notre ressource est la clé pour éliminer la duplication de code: nous pouvons utiliser le code du constructeur de copie pour faire la copie, et nous n'avons jamais besoin de répéter la moindre partie. Maintenant que la copie est faite, nous sommes prêts à échanger.

Observez qu'en entrant dans la fonction, toutes les nouvelles données sont déjà allouées, copiées et prêtes à être utilisées. C'est ce qui nous donne une forte garantie d'exception gratuite: nous n'entrerons même pas dans la fonction si la construction de la copie échoue, et il n'est donc pas possible de modifier l'état de *this. (Ce que nous avons fait manuellement auparavant pour une garantie d'exception forte, le compilateur le fait pour nous maintenant; quelle gentillesse.)

À ce stade, nous sommes sans domicile, car il swapne lance pas. Nous échangeons nos données actuelles avec les données copiées, modifiant en toute sécurité notre état, et les anciennes données sont mises dans le temporaire. Les anciennes données sont ensuite libérées lorsque la fonction revient. (Où se termine la portée du paramètre et où son destructeur est appelé.)

Parce que l'idiome ne répète aucun code, nous ne pouvons pas introduire de bogues dans l'opérateur. Notez que cela signifie que nous sommes débarrassés de la nécessité d'un contrôle d'auto-affectation, permettant une seule mise en œuvre uniforme deoperator= . (De plus, nous n'avons plus de pénalité de performance sur les non-auto-affectations.)

Et c'est l'idiome de copie et d'échange.

Et C ++ 11?

La prochaine version de C ++, C ++ 11, apporte un changement très important à la façon dont nous gérons les ressources: la règle de trois est désormais la règle de quatre (et demi). Pourquoi? Parce que non seulement nous devons être capables de copier-construire notre ressource, nous devons aussi la déplacer-construire .

Heureusement pour nous, c'est facile:

class dumb_array
{
public:
    // ...

    // move constructor
    dumb_array(dumb_array&& other) noexcept ††
        : dumb_array() // initialize via default constructor, C++11 only
    {
        swap(*this, other);
    }

    // ...
};

Que se passe t-il ici? Rappelez-vous l'objectif de la construction de mouvements: prendre les ressources d'une autre instance de la classe, en la laissant dans un état garanti affectable et destructible.

Donc, ce que nous avons fait est simple: initialiser via le constructeur par défaut (une fonctionnalité C ++ 11), puis échanger avec other; nous savons qu'une instance construite par défaut de notre classe peut être affectée et détruite en toute sécurité, nous savons otherdonc qu'elle pourra faire de même après l'échange.

(Notez que certains compilateurs ne prennent pas en charge la délégation de constructeur; dans ce cas, nous devons manuellement construire par défaut la classe. C'est une tâche malheureuse mais heureusement triviale.)

Pourquoi ça marche?

C'est le seul changement que nous devons apporter à notre classe, alors pourquoi ça marche? Rappelez-vous la décision toujours importante que nous avons prise pour faire du paramètre une valeur et non une référence:

dumb_array& operator=(dumb_array other); // (1)

Maintenant, si otherest initialisé avec une valeur r, il sera construit par déplacement . Parfait. De la même manière que C ++ 03 réutilisons notre fonctionnalité de constructeur de copie en prenant la valeur d'argument, C ++ 11 choisira automatiquement le constructeur de déplacement le cas échéant. (Et, bien sûr, comme mentionné dans un article précédemment lié, la copie / le déplacement de la valeur peut simplement être complètement évité.)

Et ainsi conclut l'idiome de copie et d'échange.


Notes de bas de page

* Pourquoi définissons-nous la valeur mArraynull? Parce que si un autre code de l'opérateur est lancé, le destructeur de dumb_arraypourrait être appelé; et si cela se produit sans le mettre à null, nous essayons de supprimer la mémoire qui a déjà été supprimée! Nous évitons cela en le définissant sur null, car la suppression de null est une non-opération.

† Il y a d' autres demandes que nous devons spécialiser std::swappour notre type, fournir une en classe le swaplong de côté une zone de libre-fonction swap, etc. Mais tout cela est inutile: toute utilisation appropriée de swapse fera par un appel non qualifié, et notre fonction sera trouvé via ADL . Une fonction fera l'affaire.

‡ La raison est simple: une fois que vous avez la ressource pour vous, vous pouvez l'échanger et / ou la déplacer (C ++ 11) partout où elle doit être. Et en faisant la copie dans la liste des paramètres, vous optimisez l'optimisation.

†† Le constructeur de déplacement doit généralement l'être noexcept, sinon un code (par exemple, une std::vectorlogique de redimensionnement) utilisera le constructeur de copie même lorsqu'un déplacement est logique. Bien sûr, ne le marquez pas sauf si le code qu'il contient ne lève pas d'exceptions.

GManNickG
la source
17
@GMan: Je dirais qu'une classe gérant plusieurs ressources à la fois est vouée à l'échec (la sécurité des exceptions devient cauchemardesque) et je recommanderais fortement qu'une classe gère UNE ressource OU qu'elle ait des fonctionnalités métier et utilise des gestionnaires.
Matthieu M.
22
Je ne comprends pas pourquoi la méthode de swap est déclarée comme ami ici?
szx
9
@asd: pour permettre de le trouver via ADL.
GManNickG
8
@neuviemeporte: Avec la parenthèse, les éléments des tableaux sont initialisés par défaut. Sans, ils ne sont pas initialisés. Étant donné que dans le constructeur de copie, nous remplacerons les valeurs de toute façon, nous pouvons ignorer l'initialisation.
GManNickG
10
@neuviemeporte: Vous devez swapêtre trouvé lors de l'ADL si vous voulez qu'il fonctionne dans la plupart des codes génériques que vous rencontrerez, comme boost::swapet dans diverses autres instances de swap. Le swap est un problème délicat en C ++, et en général, nous sommes tous tombés d'accord pour dire qu'un seul point d'accès est le meilleur (pour la cohérence), et la seule façon de le faire en général est une fonction libre ( intne peut pas avoir de membre swap, par exemple). Voir ma question pour un peu de contexte.
GManNickG
274

L'affectation, en son cœur, est en deux étapes: démolir l'ancien état de l'objet et construire son nouvel état comme une copie de l'état d'un autre objet.

Fondamentalement, c'est ce que font le destructeur et le constructeur de copie , donc la première idée serait de leur déléguer le travail. Cependant, étant donné que la destruction ne doit pas échouer, alors que la construction pourrait le faire, nous voulons en fait procéder dans l'autre sens : effectuer d'abord la partie constructive et, si cela réussit, faire la partie destructrice . L'idiome de copie et d'échange est un moyen de faire exactement cela: il appelle d'abord le constructeur de copie d'une classe pour créer un objet temporaire, puis échange ses données avec le temporaire, puis laisse le destructeur du temporaire détruire l'ancien état.
Depuisswap()est censé ne jamais échouer, la seule partie qui pourrait échouer est la copie-construction. Cela est effectué en premier, et s'il échoue, rien ne sera modifié dans l'objet ciblé.

Dans sa forme raffinée, la copie et l'échange sont implémentés en faisant effectuer la copie en initialisant le paramètre (non référence) de l'opérateur d'affectation:

T& operator=(T tmp)
{
    this->swap(tmp);
    return *this;
}
sbi
la source
1
Je pense que mentionner le bouton est aussi important que mentionner la copie, l'échange et la destruction. Le swap n'est pas comme par magie à l'abri des exceptions. Il est à l'abri des exceptions, car l'échange de pointeurs est à l'exception. Vous n'êtes pas obligé d'utiliser un bouton, mais si vous ne l'utilisez pas, vous devez vous assurer que chaque échange d'un membre est protégé contre les exceptions. Cela peut être un cauchemar lorsque ces membres peuvent changer et c'est trivial lorsqu'ils sont cachés derrière un bouton. Et puis, vient ensuite le coût du bouton. Ce qui nous amène à la conclusion que la sécurité d'exception a souvent un coût de performance.
wilhelmtell
7
std::swap(this_string, that)ne fournit pas de garantie anti-projection. Il offre une forte sécurité d'exception, mais pas une garantie anti-projection.
wilhelmtell
11
@wilhelmtell: En C ++ 03, il n'y a aucune mention d'exceptions potentiellement levées par std::string::swap(qui est appelée par std::swap). En C ++ 0x, std::string::swapest noexceptet ne doit pas lever d'exceptions.
James McNellis
2
@sbi @JamesMcNellis ok, mais le point demeure: si vous avez des membres de type classe, vous devez vous assurer que les échanger est un non-lancer. Si vous avez un seul membre qui est un pointeur, c'est trivial. Sinon, ce n'est pas le cas.
wilhelmtell
2
@wilhelmtell: Je pensais que c'était le point de l'échange: ça ne lançait jamais et c'était toujours O (1) (ouais, je sais, std::array...)
sbi
44

Il y a déjà de bonnes réponses. Je vais me concentrer principalement sur ce qui, selon moi, leur manque - une explication des «inconvénients» avec l'idiome de copie et d'échange ...

Qu'est-ce que l'idiome de copie et d'échange?

Une façon d'implémenter l'opérateur d'affectation en termes de fonction de 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 consiste à s'assurer que toutes les ressources dont le nouvel état a besoin sont acquises (par exemple, la mémoire, les descripteurs)

  • cette acquisition peut être tentée avant de modifier l'état actuel de l'objet (c.-à-d. *this) si une copie de la nouvelle valeur est effectuée, c'est pourquoi elle rhsest acceptée par valeur (c.-à-d. copiée) plutôt que par référence

  • permuter l'état de la copie locale rhset *thisest généralement relativement facile à faire sans échec / exceptions potentiels, étant donné que la copie locale n'a pas besoin d'un état particulier par la suite (a juste besoin d'un état approprié pour que le destructeur s'exécute, tout comme pour un objet déplacé de dans> = C ++ 11)

Quand faut-il l'utiliser? (Quels problèmes résout-il [/ crée- t-il ] ?)

  • Lorsque vous souhaitez que l'objet affecté ne soit pas affecté par une affectation qui lève une exception, en supposant que vous avez ou pouvez écrire une swapgarantie d'exception forte, et idéalement une qui ne peut pas échouer / throw.. †

  • Lorsque vous voulez une façon claire, facile à comprendre et robuste de définir l'opérateur d'affectation en termes de constructeur de copie (plus simple) swapet de fonctions de destructeur.

    • L'auto-affectation effectuée en tant que copie et échange évite les cas marginaux souvent négligés. ‡

  • Lorsqu'une pénalité de performance ou une utilisation momentanément plus élevée des ressources créée par la présence d'un objet temporaire supplémentaire pendant l'affectation n'est pas importante pour votre application. ⁂

swaplancer: il est généralement possible d'échanger de manière fiable des membres de données que les objets suivent par un pointeur, mais des membres de données non pointeurs qui n'ont pas un échange sans projection, ou pour lesquels l'échange doit être implémenté en tant X tmp = lhs; lhs = rhs; rhs = tmp;que copie-construction ou affectation peuvent jeter, ont toujours le potentiel d'échouer en laissant certains membres de données échangés et d'autres non. Ce potentiel s'applique même aux C ++ 03 std::stringcomme James commente une autre réponse:

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


‡ L'implémentation de l'opérateur d'affectation qui semble raisonnable lors de l'attribution à partir d'un objet distinct peut facilement échouer pour l'auto-affectation. Bien qu'il puisse sembler inimaginable que le code client tente même de s'auto-affecter, cela peut se produire relativement facilement pendant les opérations algo sur les conteneurs, avec du x = f(x);code où fest (peut-être seulement pour certaines #ifdefbranches) une macro ala #define f(x) xou une fonction renvoyant une référence à x, ou même (probablement inefficace mais concis) comme du code 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'auto-affectation, les suppressions de code ci-dessus x.p_;pointent p_vers une région de tas nouvellement allouée, puis tentent de lire les données non initialisées qui s'y trouvent (Comportement non défini), si cela ne fait rien de trop bizarre, copytente une auto-affectation à chaque 'T' détruit!


⁂ L'idiome de copie et d'échange peut introduire des inefficacités ou des limitations en raison de 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, une écriture manuscrite Client::operator=pourrait vérifier si elle *thisest déjà connectée au même serveur que rhs(peut-être en envoyant un code "reset" si utile), tandis que l'approche copier-et-échanger invoquerait le constructeur de copie qui serait probablement écrit pour s'ouvrir une connexion socket distincte puis fermez l'original. Non seulement cela pourrait signifier une interaction réseau à distance au lieu d'une simple copie de variable en cours de traitement, mais cela pourrait également aller à l'encontre des limites du client ou du serveur sur les ressources ou les connexions de socket. (Bien sûr, cette classe a une interface assez horrible, mais c'est une autre affaire ;-P).

Tony Delroy
la source
4
Cela dit, une connexion socket n'est qu'un exemple - le même principe s'applique à toute initialisation potentiellement coûteuse, telle que la vérification / l'initialisation / l'étalonnage du matériel, la génération d'un pool de threads ou de nombres aléatoires, certaines tâches de cryptographie, les caches, les analyses du système de fichiers, la base de données connexions etc ..
Tony Delroy
Il y a un autre (massif) con. Comme des spécifications actuelles sur le plan technique l'objet sera pas un opérateur mouvement affectation! Si elle est utilisée plus tard comme membre d'une classe, la nouvelle classe n'aura pas de move-ctor généré automatiquement! Source: youtu.be/mYrbivnruYw?t=43m14s
user362515
3
Le principal problème avec l'opérateur d'affectation de copie de Clientest que l'affectation n'est pas interdite.
sbi
Dans l'exemple client, la classe doit être rendue non copiable.
John Z. Li
25

Cette réponse ressemble plus à 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 y a un bogue qui est vraiment ennuyeux et n'a pas de sens. Donc, si vous déclarez / définissez votre swapfonction comme ceci:

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

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

}

... le compilateur vous crie dessus lorsque vous appelez la swapfonction:

entrez la description de l'image ici

Cela a quelque chose à voir avec l' friendappel d' une fonction et la thistransmission d'un objet en tant que paramètre.


Un moyen de contourner cela est de ne pas utiliser de friendmot-clé et de redéfinir la swapfonction:

void swap(A& other) {

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

}

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

entrez la description de l'image ici


Après tout, vous n'avez pas besoin d'utiliser une friendfonction pour échanger 2 objets. Il est tout aussi logique de créer swapune fonction membre ayant un otherobjet comme paramètre.

Vous avez déjà accès à l' thisobjet, donc le transmettre en tant que paramètre est techniquement redondant.

Oleksiy
la source
1
@GManNickG dropbox.com/s/o1mitwcpxmawcot/example.cpp dropbox.com/s/jrjrn5dh1zez5vy/Untitled.jpg . Il s'agit d'une version simplifiée. Une erreur semble se produire chaque fois qu'une friendfonction est appelée avec un *thisparamètre
Oleksiy
1
@GManNickG comme je l'ai dit, c'est un bug et pourrait bien fonctionner pour d'autres personnes. Je voulais juste aider certaines personnes qui pourraient avoir le même problème que moi. J'ai essayé cela avec Visual Studio 2012 Express et 2013 Preview et la seule chose qui l'a fait disparaître, c'est ma modification
Oleksiy
8
@GManNickG, il ne rentrerait pas dans un commentaire avec toutes les images et les exemples de code. Et ça va si les gens votent, je suis sûr qu'il y a quelqu'un là-bas qui a le même bug; les informations contenues dans ce message pourraient être exactement ce dont elles ont besoin.
Oleksiy
14
notez que ce n'est qu'un bug dans la mise en évidence du code IDE (IntelliSense) ... Il se compilera très bien sans avertissements / erreurs.
Amro
3
Veuillez signaler le bogue VS ici si vous ne l'avez pas déjà fait (et s'il n'a pas été corrigé) connect.microsoft.com/VisualStudio
Matt
15

Je voudrais ajouter un mot d'avertissement lorsque vous traitez avec des conteneurs compatibles avec l'allocateur de style C ++ 11. L'échange et l'affectation ont une sémantique subtilement différente.

Pour être concret, considérons un conteneur std::vector<T, A>, où se Atrouve 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 fset fmest de donner al'état qui bavait 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: cela dépend. Écrivons AT = std::allocator_traits<A>.

  • Si AT::propagate_on_container_move_assignmentest std::true_type, fmréaffecte alors l'allocateur de aavec la valeur de b.get_allocator(), sinon il ne le fait pas et acontinue d'utiliser son allocateur d'origine. Dans ce cas, les éléments de données doivent être échangés individuellement, car le stockage de aet bn'est pas compatible.

  • Si AT::propagate_on_container_swapc'est le cas std::true_type, fséchange les données et les allocateurs de la manière attendue.

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

    • Si a.get_allocator() == b.get_allocator(), alors les deux conteneurs utilisent un stockage compatible et l'échange se déroule de la manière habituelle.
    • Cependant, si a.get_allocator() != b.get_allocator(), le programme a un comportement indéfini (cf. [container.requirements.general / 8].

Le résultat est que l'échange est devenu une opération non triviale en C ++ 11 dès que votre conteneur commence à prendre en charge les allocateurs avec état. C'est un cas d'utilisation quelque peu avancé, mais ce n'est pas tout à fait improbable, car les optimisations de déplacement ne deviennent généralement intéressantes qu'une fois que votre classe gère une ressource, et la mémoire est l'une des ressources les plus populaires.

Kerrek SB
la source