Copier le constructeur et l'opérateur = surcharge en C ++: une fonction commune est-elle possible?

87

Depuis un constructeur de copie

MyClass(const MyClass&);

et an = surcharge de l'opérateur

MyClass& operator = (const MyClass&);

ont à peu près le même code, le même paramètre, et ne diffèrent que sur le retour, est-il possible d'avoir une fonction commune pour les deux à utiliser?

MPelletier
la source
6
"... ont à peu près le même code ..."? Hmm ... Vous devez faire quelque chose de mal. Essayez de minimiser le besoin d'utiliser des fonctions définies par l'utilisateur pour cela et laissez le compilateur faire tout le sale boulot. Cela signifie souvent encapsuler les ressources dans leur propre objet membre. Vous pourriez nous montrer du code. Peut-être avons-nous de bonnes suggestions de conception.
sellibitze
2
Double possible de la duplication de code
mpromonet

Réponses:

121

Oui. Il existe deux options courantes. L'une - qui est généralement déconseillée - consiste à appeler operator=explicitement le constructeur de copie:

MyClass(const MyClass& other)
{
    operator=(other);
}

Cependant, fournir un bien operator=est un défi lorsqu'il s'agit de gérer l'ancien état et les problèmes liés à l'auto-affectation. En outre, tous les membres et bases sont initialisés par défaut en premier, même s'ils doivent être affectés à from other. Cela peut même ne pas être valable pour tous les membres et bases et même là où c'est valable, cela est sémantiquement redondant et peut être pratiquement coûteux.

Une solution de plus en plus populaire consiste à implémenter en operator=utilisant le constructeur de copie et une méthode d'échange.

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

ou même:

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

Une swapfonction est généralement simple à écrire car elle échange simplement la propriété des composants internes et n'a pas à nettoyer l'état existant ou à allouer de nouvelles ressources.

Les avantages de l'idiome de copie et d'échange est qu'il est automatiquement sûr pour l'auto-affectation et - à condition que l'opération d'échange ne soit pas lancée - est également fortement protégé contre les exceptions.

Pour être parfaitement sûr d'exception, un opérateur d'affectation écrit `` à la main '' doit généralement allouer une copie des nouvelles ressources avant de désallouer les anciennes ressources du cessionnaire de sorte que si une exception se produit en allouant les nouvelles ressources, l'ancien état peut toujours être retourné à . Tout cela est gratuit avec copie et échange, mais est généralement plus complexe, et donc sujet aux erreurs, à faire à partir de zéro.

La seule chose à laquelle il faut faire attention est de s'assurer que la méthode swap est un vrai swap, et non la méthode par défaut std::swapqui utilise le constructeur de copie et l'opérateur d'affectation lui-même.

En règle générale, un membre swapest utilisé. std::swapfonctionne et est garanti «no-throw» avec tous les types de base et les types de pointeurs. La plupart des pointeurs intelligents peuvent également être échangés avec une garantie sans jet.

CB Bailey
la source
3
En fait, ce ne sont pas des opérations courantes. Alors que le contrôleur de copie initialise les membres de l'objet pour la première fois, l'opérateur d'affectation remplace les valeurs existantes. Compte tenu de cela, tout à operator=partir du cteur de copie est en fait assez mauvais, car il initialise d'abord toutes les valeurs avec une valeur par défaut juste pour les remplacer par les valeurs de l'autre objet juste après.
sbi
14
Peut-être à "Je ne recommande pas", ajoutez "et aucun expert C ++ non plus". Quelqu'un pourrait venir et ne pas se rendre compte que vous n'exprimez pas seulement une préférence personnelle minoritaire, mais l'opinion consensuelle établie de ceux qui y ont réellement réfléchi. Et, OK, peut-être que je me trompe et qu'un expert C ++ le recommande, mais personnellement, je poserais toujours le gant à quelqu'un de proposer une référence pour cette recommandation.
Steve Jessop
4
Assez juste, je vous ai déjà voté de toute façon :-). Je pense que si quelque chose est largement considéré comme une meilleure pratique, il est préférable de le dire (et de le revoir si quelqu'un dit que ce n'est pas vraiment mieux après tout). De même, si quelqu'un a demandé "est-il possible d'utiliser des mutex en C ++", je ne dirais pas "une option assez courante consiste à ignorer complètement RAII et à écrire du code non protégé contre les exceptions qui bloque la production, mais il est de plus en plus populaire d'écrire code de travail décent ";-)
Steve Jessop
4
+1. Et je pense qu'une analyse est toujours nécessaire. Je pense qu'il est raisonnable d'avoir une assignfonction membre utilisée à la fois par le cteur de copie et l'opérateur d'affectation dans certains cas (pour les classes légères). Dans d'autres cas (cas d'utilisation intensive de ressources, descripteur / corps), une copie / échange est la voie à suivre bien sûr.
Johannes Schaub - litb
2
@litb: J'ai été surpris par cela, alors j'ai recherché l'élément 41 dans Exception C ++ (auquel ce gotw s'est transformé) et cette recommandation particulière a disparu et il recommande de copier-et-échanger à sa place. Plutôt sournoisement, il a abandonné "Problème n ° 4: Il est inefficace pour l'affectation" en même temps.
CB Bailey
13

Le constructeur de copie effectue la première initialisation des objets qui étaient auparavant de la mémoire brute. L'opérateur d'affectation, OTOH, remplace les valeurs existantes par de nouvelles. Le plus souvent, cela implique de rejeter les anciennes ressources (par exemple, la mémoire) et d'en allouer de nouvelles.

S'il y a une similitude entre les deux, c'est que l'opérateur d'affectation effectue la destruction et la copie-construction. Certains développeurs avaient l'habitude de mettre en œuvre l'attribution par destruction sur place suivie d'une construction de copie de placement. Cependant, c'est une très mauvaise idée. (Que faire s'il s'agit de l'opérateur d'affectation d'une classe de base qui a appelé lors de l'affectation d'une classe dérivée?)

Ce qui est généralement considéré comme l'idiome canonique de nos jours utilise swapcomme Charles l'a suggéré:

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

Cela utilise la construction par copie (notez que otherc'est copié) et la destruction (c'est détruit à la fin de la fonction) - et il les utilise aussi dans le bon ordre: construction (peut échouer) avant la destruction (ne doit pas échouer).

sbi
la source
Doit swapêtre déclaré virtual?
1
@Johannes: Les fonctions virtuelles sont utilisées dans les hiérarchies de classes polymorphes. Les opérateurs d'affectation sont utilisés pour les types valeur. Les deux se mélangent à peine.
sbi
-3

Quelque chose me dérange:

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

Premièrement, lire le mot «swap» quand mon esprit pense «copier» irrite mon bon sens. Aussi, je remets en question le but de cette astuce sophistiquée. Oui, toutes les exceptions dans la construction des nouvelles ressources (copiées) devraient se produire avant l'échange, ce qui semble être un moyen sûr de s'assurer que toutes les nouvelles données sont remplies avant de les mettre en ligne.

C'est très bien. Alors, qu'en est-il des exceptions qui se produisent après l'échange? (lorsque les anciennes ressources sont détruites lorsque l'objet temporaire est hors de portée) Du point de vue de l'utilisateur de l'affectation, l'opération a échoué, sauf que ce n'est pas le cas. Cela a un énorme effet secondaire: la copie a effectivement eu lieu. Seul un nettoyage des ressources a échoué. L'état de l'objet de destination a été modifié même si l'opération semble avoir échoué de l'extérieur.

Donc, je propose au lieu de "swap" de faire un "transfert" plus naturel:

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    transfer(tmp);
    return *this;
}

Il y a toujours la construction de l'objet temporaire, mais la prochaine action immédiate est de libérer toutes les ressources actuelles de la destination avant de déplacer (et de NULL pour qu'elles ne soient pas libérées deux fois) les ressources de la source vers elle.

Au lieu de {construire, déplacer, détruire}, je propose {construire, détruire, déplacer}. Le mouvement, qui est l'action la plus dangereuse, est celui qui est effectué en dernier après que tout le reste a été réglé.

Oui, l'échec de la destruction est un problème dans les deux schémas. Les données sont soit corrompues (copiées lorsque vous ne pensiez pas qu'elles l'étaient) ou perdues (libérées lorsque vous ne pensiez pas qu'elles l'étaient). Perdu vaut mieux que corrompu. Aucune donnée n'est meilleure que de mauvaises données.

Transférer au lieu de swap. C'est ma suggestion de toute façon.

Matthieu
la source
2
Un destructeur ne doit pas échouer, donc des exceptions lors de la destruction ne sont pas attendues. Et, je ne comprends pas quel serait l'avantage de déplacer le mouvement derrière la destruction, si le mouvement est l'opération la plus dangereuse? C'est-à-dire que dans le schéma standard, un échec de déplacement ne corrompra pas l'ancien état, contrairement à votre nouveau schéma. Alors pourquoi? Aussi, First, reading the word "swap" when my mind is thinking "copy" irritates-> En tant qu'écrivain de bibliothèque, vous connaissez généralement les pratiques courantes (copie + échange), et le point crucial est my mind. Votre esprit est en fait caché derrière l'interface publique. C'est ce qu'est le code réutilisable.
Sebastian Mach